diff --git a/src/adaptix/_internal/morphing/concrete_provider.py b/src/adaptix/_internal/morphing/concrete_provider.py index 11650485..aa029274 100644 --- a/src/adaptix/_internal/morphing/concrete_provider.py +++ b/src/adaptix/_internal/morphing/concrete_provider.py @@ -9,19 +9,25 @@ from io import BytesIO from typing import Generic, Optional, TypeVar, Union +from adaptix._internal.utils import Omittable, Omitted + +from ... import DebugTrail from ..common import Dumper, Loader from ..feature_requirement import HAS_PY_311, HAS_SELF_TYPE from ..provider.essential import CannotProvide, Mediator from ..provider.loc_stack_filtering import P, create_loc_stack_checker from ..provider.loc_stack_tools import find_owner_with_field from ..provider.located_request import LocatedRequest, for_predicate +from ..provider.location import GenericParamLoc from ..special_cases_optimization import as_is_stub +from .dump_error import SentinelDumpError from .json_schema.definitions import JSONSchema from .json_schema.request_cls import JSONSchemaRequest from .json_schema.schema_model import JSONSchemaBuiltinFormat, JSONSchemaType -from .load_error import FormatMismatchLoadError, TypeLoadError, ValueLoadError +from .load_error import FormatMismatchLoadError, LoadError, TypeLoadError, UnionLoadError, ValueLoadError from .provider_template import DumperProvider, JSONSchemaProvider, MorphingProvider -from .request_cls import DumperRequest, LoaderRequest, StrictCoercionRequest +from .request_cls import DebugTrailRequest, DumperRequest, LoaderRequest, StrictCoercionRequest +from .utils import try_normalize_type class IsoFormatProvider(MorphingProvider): @@ -633,3 +639,27 @@ def provide_dumper(self, mediator: Mediator, request: DumperRequest) -> Dumper: def _generate_json_schema(self, mediator: Mediator, request: JSONSchemaRequest) -> JSONSchema: return JSONSchema(type=JSONSchemaType.STRING) + + +def make_sentinel_dumper(sentinel_type: typing.Type): + def sentinel_dumper(data): + if type(data) is sentinel_type: + raise SentinelDumpError(sentinel_type) + return sentinel_dumper + + +def sentinel_loader(data): + raise ValueLoadError("Field value required", data) + + +@for_predicate(Omitted) +class OmittedProvider(MorphingProvider): + def _generate_json_schema(self, mediator: Mediator, request: JSONSchemaRequest) -> JSONSchema: + raise CannotProvide + + def provide_dumper(self, mediator: Mediator[Dumper], request: DumperRequest) -> Dumper: + return make_sentinel_dumper(Omitted) + + + def provide_loader(self, mediator: Mediator[Loader], request: LoaderRequest) -> Loader: + return sentinel_loader diff --git a/src/adaptix/_internal/morphing/dump_error.py b/src/adaptix/_internal/morphing/dump_error.py new file mode 100644 index 00000000..2235c8d6 --- /dev/null +++ b/src/adaptix/_internal/morphing/dump_error.py @@ -0,0 +1,12 @@ +import dataclasses + +from adaptix._internal.utils import fix_dataclass_from_builtin + + +@dataclasses.dataclass(eq=False) +@fix_dataclass_from_builtin +class SentinelDumpError(Exception): + sentinel: type + + def __str__(self): + return f"Cannot dump {self.sentinel!r}" diff --git a/src/adaptix/_internal/morphing/facade/retort.py b/src/adaptix/_internal/morphing/facade/retort.py index 8d2f1658..34fd3464 100644 --- a/src/adaptix/_internal/morphing/facade/retort.py +++ b/src/adaptix/_internal/morphing/facade/retort.py @@ -32,6 +32,7 @@ IsoFormatProvider, LiteralStringProvider, NoneProvider, + OmittedProvider, RegexPatternProvider, SecondsTimedeltaProvider, SelfTypeProvider, @@ -136,6 +137,7 @@ class FilledRetort(OperatingRetort, ABC): RegexPatternProvider(), SelfTypeProvider(), LiteralStringProvider(), + OmittedProvider(), ABCProxy(Mapping, dict), ABCProxy(MutableMapping, dict), diff --git a/src/adaptix/_internal/morphing/model/basic_gen.py b/src/adaptix/_internal/morphing/model/basic_gen.py index 82efa7ae..f8feb202 100644 --- a/src/adaptix/_internal/morphing/model/basic_gen.py +++ b/src/adaptix/_internal/morphing/model/basic_gen.py @@ -124,7 +124,9 @@ def get_skipped_fields(shape: BaseShape, name_layout: BaseNameLayout) -> Set[str extra_targets = name_layout.extra_move.fields if isinstance(name_layout.extra_move, ExtraTargets) else () return { field.id for field in shape.fields - if field.id not in used_direct_fields and field.id not in extra_targets + if field.id not in used_direct_fields + and field.id not in extra_targets + } diff --git a/tests/unit/morphing/test_concrete_provider.py b/tests/unit/morphing/test_concrete_provider.py index 60f06c5a..a5179889 100644 --- a/tests/unit/morphing/test_concrete_provider.py +++ b/tests/unit/morphing/test_concrete_provider.py @@ -9,14 +9,17 @@ import pytest from tests_helpers import cond_list, raises_exc +from unit.integrations.sqlalchemy.test_orm import retort -from adaptix import Retort +from adaptix import DebugTrail, Omittable, Omitted, Retort from adaptix._internal.feature_requirement import HAS_PY_311, IS_PYPY from adaptix._internal.morphing.concrete_provider import ( DatetimeFormatProvider, DateTimestampProvider, DatetimeTimestampProvider, ) +from adaptix._internal.morphing.dump_error import SentinelDumpError +from adaptix._internal.morphing.load_error import LoadError, UnionLoadError from adaptix.load_error import FormatMismatchLoadError, TypeLoadError, ValueLoadError INVALID_INPUT_ISO_FORMAT = ( @@ -514,3 +517,41 @@ def test_complex_loader_provider(strict_coercion, debug_trail): raises_exc(ValueLoadError("Bad string format", "foo"), lambda: loader("foo")) raises_exc(TypeLoadError(Union[str, complex], None), lambda: loader(None)) raises_exc(TypeLoadError(Union[str, complex], []), lambda: loader([])) + + +def test_omittable_provider(strict_coercion, debug_trail): + retort = Retort( + strict_coercion=strict_coercion, + debug_trail=debug_trail, + ) + + dumper = retort.get_dumper(Omittable[int]) + raises_exc( + SentinelDumpError(Omitted), + lambda: dumper(Omitted()), + ) + loader = retort.get_loader(Omittable[int]) + assert loader(1) == 1 + + if debug_trail == DebugTrail.DISABLE: + raises_exc( + LoadError(), + lambda: loader(Omitted()), + ) + raises_exc( + LoadError(), + lambda: loader([]), + ) + elif debug_trail in (DebugTrail.FIRST, DebugTrail.ALL): + if not strict_coercion: + return + raises_exc( + UnionLoadError( + f"while loading {Union[int, Omitted]}", + [ + ValueLoadError("Field value required", "100"), + TypeLoadError(int, "100"), + ], + ), + lambda: loader("100"), + )