Skip to content

Commit

Permalink
add get_converter high-level function to create converters
Browse files Browse the repository at this point in the history
add validation that impl_converter is applied on stub function
impl_converter now is coping attributes of stub function
refactor model_spec system, use it with converter testing
add more tests, fix bugs
  • Loading branch information
zhPavel committed Feb 4, 2024
1 parent d59168d commit d5c242b
Show file tree
Hide file tree
Showing 18 changed files with 359 additions and 158 deletions.
33 changes: 24 additions & 9 deletions src/adaptix/_internal/conversion/broaching/code_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@
from abc import ABC, abstractmethod
from ast import AST
from collections import defaultdict
from functools import update_wrapper
from inspect import Signature
from typing import DefaultDict, Mapping, Tuple, Union
from typing import Callable, DefaultDict, Mapping, Optional, Tuple, Union

from ...code_tools.ast_templater import ast_substitute
from ...code_tools.cascade_namespace import BuiltinCascadeNamespace, CascadeNamespace
Expand Down Expand Up @@ -56,7 +57,12 @@ def register_mangled(self, base: str, obj: object) -> str:

class BroachingCodeGenerator(ABC):
@abstractmethod
def produce_code(self, closure_name: str, signature: Signature) -> Tuple[str, Mapping[str, object]]:
def produce_code(
self,
signature: Signature,
closure_name: str,
stub_function: Optional[Callable],
) -> Tuple[str, Mapping[str, object]]:
...


Expand All @@ -69,12 +75,19 @@ def _create_state(self, ctx_namespace: CascadeNamespace) -> GenState:
ctx_namespace=ctx_namespace,
)

def produce_code(self, closure_name: str, signature: Signature) -> Tuple[str, Mapping[str, object]]:
def produce_code(
self,
signature: Signature,
closure_name: str,
stub_function: Optional[Callable],
) -> Tuple[str, Mapping[str, object]]:
builder = CodeBuilder()
ctx_namespace = BuiltinCascadeNamespace(occupied=signature.parameters.keys())
state = self._create_state(ctx_namespace=ctx_namespace)

ctx_namespace.add_constant('_closure_signature', signature)
ctx_namespace.add_constant('_stub_function', stub_function)
ctx_namespace.add_constant('_update_wrapper', update_wrapper)
no_types_signature = signature.replace(
parameters=[param.replace(annotation=Signature.empty) for param in signature.parameters.values()],
return_annotation=Signature.empty,
Expand All @@ -83,7 +96,11 @@ def produce_code(self, closure_name: str, signature: Signature) -> Tuple[str, Ma
body = self._gen_plan_element_dispatch(state, self._plan)
builder += 'return ' + compat_ast_unparse(body)

if stub_function is not None:
builder += f'_update_wrapper({closure_name}, _stub_function)'

builder += f'{closure_name}.__signature__ = _closure_signature'
builder += f'{closure_name}.__name__ = {closure_name!r}'
return builder.string(), ctx_namespace.constants

def _gen_plan_element_dispatch(self, state: GenState, element: BroachingPlan) -> AST:
Expand Down Expand Up @@ -159,12 +176,10 @@ def _gen_accessor_element(self, state: GenState, element: AccessorElement[Broach
)

if isinstance(element.accessor, ItemAccessor):
literal_expr = get_literal_expr(element.accessor.key)
if literal_expr is not None:
return ast_substitute(
f"__target_expr__[{literal_expr!r}]",
target_expr=target_expr,
)
return ast_substitute(
f"__target_expr__[{element.accessor.key!r}]",
target_expr=target_expr,
)

name = state.register_next_id('accessor', element.accessor.getter)
return ast_substitute(
Expand Down
9 changes: 8 additions & 1 deletion src/adaptix/_internal/conversion/converter_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,8 +84,9 @@ def _make_converter(
code_gen = self._create_broaching_code_gen(broaching_plan)
closure_name = self._get_closure_name(request)
dumper_code, dumper_namespace = code_gen.produce_code(
closure_name=closure_name,
signature=request.signature,
closure_name=closure_name,
stub_function=request.stub_function,
)
code_gen_loc_stack = LocStack(LocMap(TypeHintLoc(self._get_type_from_annotation(signature.return_annotation))))
return compile_closure_with_globals_capturing(
Expand All @@ -100,13 +101,19 @@ def _make_converter(
def _get_closure_name(self, request: ConverterRequest) -> str:
if request.function_name is not None:
return request.function_name
stub_function_name = getattr(request.stub_function, '__name__', None)
if stub_function_name is not None:
return stub_function_name
src = next(iter(request.signature.parameters.values()))
dst = self._get_type_from_annotation(request.signature.return_annotation)
return self._name_sanitizer.sanitize(f'convert_{src}_to_{dst}')

def _get_file_name(self, request: ConverterRequest) -> str:
if request.function_name is not None:
return request.function_name
stub_function_name = getattr(request.stub_function, '__name__', None)
if stub_function_name is not None:
return stub_function_name
src = next(iter(request.signature.parameters.values()))
dst = self._get_type_from_annotation(request.signature.return_annotation)
return f'convert_{src}_to_{dst}'
Expand Down
18 changes: 18 additions & 0 deletions src/adaptix/_internal/conversion/facade/checker.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import ast
import inspect
from textwrap import dedent


def ensure_function_is_stub(func):
source = dedent(inspect.getsource(func))
ast_module = ast.parse(source)
func_body = ast_module.body[0].body
if len(func_body) == 1:
body_element = func_body[0]
if isinstance(body_element, ast.Pass):
return
if isinstance(body_element, ast.Expr) and isinstance(body_element.value, ast.Constant):
value = body_element.value.value
if value == Ellipsis or isinstance(value, str):
return
raise ValueError('Body of function must be empty')
59 changes: 54 additions & 5 deletions src/adaptix/_internal/conversion/facade/func.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import inspect
from functools import partial
from typing import Callable, Iterable, Optional, TypeVar, overload
from inspect import Parameter, Signature
from typing import Any, Callable, Iterable, Optional, Type, TypeVar, overload

from ...common import TypeHint
from ...provider.essential import Provider
from .checker import ensure_function_is_stub
from .retort import AdornedConverterRetort, ConverterRetort

_global_retort = ConverterRetort()
Expand All @@ -12,6 +15,50 @@
CallableT = TypeVar('CallableT', bound=Callable)


@overload
def get_converter(
src: Type[SrcT],
dst: Type[DstT],
*,
retort: AdornedConverterRetort = _global_retort,
recipe: Iterable[Provider] = (),
name: Optional[str] = None,
) -> Callable[[SrcT], DstT]:
...


@overload
def get_converter(
src: Type[TypeHint],
dst: Type[TypeHint],
*,
retort: AdornedConverterRetort = _global_retort,
recipe: Iterable[Provider] = (),
name: Optional[str] = None,
) -> Callable[[Any], Any]:
...


def get_converter(
src: TypeHint,
dst,
*,
retort: AdornedConverterRetort = _global_retort,
recipe: Iterable[Provider] = (),
name: Optional[str] = None,
):
if recipe:
retort = retort.extend(recipe=recipe)
return retort.produce_converter(
signature=Signature(
parameters=[Parameter('src', kind=Parameter.POSITIONAL_ONLY, annotation=src)],
return_annotation=dst,
),
stub_function=None,
function_name=name,
)


@overload
def impl_converter(func_stub: CallableT, /) -> CallableT:
...
Expand All @@ -27,17 +74,19 @@ def impl_converter(


def impl_converter(
func_stub: Optional[Callable] = None,
stub_function: Optional[Callable] = None,
*,
retort: AdornedConverterRetort = _global_retort,
recipe: Iterable[Provider] = (),
):
if func_stub is None:
if stub_function is None:
return partial(impl_converter, retort=retort, recipe=recipe)

if recipe:
retort = retort.extend(recipe=recipe)
ensure_function_is_stub(stub_function)
return retort.produce_converter(
signature=inspect.signature(func_stub),
function_name=getattr(func_stub, '__name__', None),
signature=inspect.signature(stub_function),
stub_function=stub_function,
function_name=None,
)
8 changes: 7 additions & 1 deletion src/adaptix/_internal/conversion/facade/retort.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,17 @@ def extend(self: AR, *, recipe: Iterable[Provider]) -> AR:

return clone

def produce_converter(self, signature: Signature, function_name: Optional[str]) -> Callable[..., Any]:
def produce_converter(
self,
signature: Signature,
stub_function: Optional[Callable],
function_name: Optional[str],
) -> Callable[..., Any]:
return self._facade_provide(
ConverterRequest(
signature=signature,
function_name=function_name,
stub_function=stub_function,
),
error_message=f'Cannot produce loader for signature {signature!r}',
)
Expand Down
3 changes: 2 additions & 1 deletion src/adaptix/_internal/conversion/request_cls.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from dataclasses import dataclass
from inspect import Signature
from itertools import chain, islice
from typing import Iterator, Optional, Union
from typing import Callable, Iterator, Optional, Union

from ..common import Coercer, VarTuple
from ..datastructures import ImmutableStack
Expand Down Expand Up @@ -61,3 +61,4 @@ class CoercerRequest(Request[Coercer]):
class ConverterRequest(Request):
signature: Signature
function_name: Optional[str]
stub_function: Optional[Callable]
4 changes: 1 addition & 3 deletions src/adaptix/_internal/morphing/model/dumper_gen.py
Original file line number Diff line number Diff line change
Expand Up @@ -190,9 +190,7 @@ def _gen_access_expr(self, namespace: CascadeNamespace, field: OutputField) -> s
return f"data.{accessor.attr_name}"
return f"getattr(data, {accessor.attr_name!r})"
if isinstance(accessor, ItemAccessor):
literal_expr = get_literal_expr(accessor.key)
if literal_expr is not None:
return f"data[{literal_expr}]"
return f"data[{accessor.key!r}]"

accessor_getter = self._v_accessor_getter(field)
namespace.add_constant(accessor_getter, field.accessor.getter)
Expand Down
3 changes: 2 additions & 1 deletion src/adaptix/conversion/__init__.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
from adaptix._internal.conversion.facade.func import impl_converter
from adaptix._internal.conversion.facade.func import get_converter, impl_converter
from adaptix._internal.conversion.facade.provider import bind, coercer
from adaptix._internal.conversion.facade.retort import AdornedConverterRetort, ConverterRetort, FilledConverterRetort

__all__ = (
'get_converter',
'impl_converter',
'bind',
'coercer',
Expand Down
23 changes: 22 additions & 1 deletion tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import pytest
from tests_helpers import ByTrailSelector
from tests_helpers import ByTrailSelector, ModelSpecSchema, parametrize_model_spec

from adaptix import DebugTrail
from adaptix._internal.feature_requirement import HAS_PY_312
Expand All @@ -20,4 +20,25 @@ def trail_select(debug_trail):
return ByTrailSelector(debug_trail)


@pytest.fixture
def model_spec() -> ModelSpecSchema:
...


@pytest.fixture
def src_model_spec() -> ModelSpecSchema:
...


@pytest.fixture
def dst_model_spec() -> ModelSpecSchema:
...


def pytest_generate_tests(metafunc):
parametrize_model_spec('model_spec', metafunc)
parametrize_model_spec('src_model_spec', metafunc)
parametrize_model_spec('dst_model_spec', metafunc)


collect_ignore_glob = [] if HAS_PY_312 else ['*_312.py']
12 changes: 12 additions & 0 deletions tests/integration/conversion/local_helpers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from enum import Enum

import pytest


class FactoryWay(Enum):
IMPL_CONVERTER = 'impl_converter'
GET_CONVERTER = 'get_converter'

@classmethod
def params(cls):
return [pytest.param(way, id=way.value) for way in cls]
Loading

0 comments on commit d5c242b

Please sign in to comment.