diff --git a/tests/test_dataclass_like.py b/tests/test_dataclass_like.py index 0f85847..e2f2202 100644 --- a/tests/test_dataclass_like.py +++ b/tests/test_dataclass_like.py @@ -8,8 +8,6 @@ import typemap_extensions as typing -import pytest - class FieldArgs(TypedDict, total=False): default: ReadOnly[object] @@ -37,6 +35,9 @@ class Field[T: FieldArgs](typing.InitField[T]): ) +# TODO: what could we do to make dataclass_ish work at runtime? + + # Begin PEP section: dataclass like __init__ """ @@ -133,30 +134,16 @@ class Hero(Model): from typemap.type_eval import format_helper -@pytest.mark.xfail(reason="UpateClass currently drops things") def test_dataclass_like_1(): tgt = eval_typing(Hero) fmt = format_helper.format_class(tgt) assert fmt == textwrap.dedent("""\ class Hero: - @classmethod - def __init_subclass__[T](cls: type[T]) -> typemap.typing.UpdateClass[InitFnType[T]]: ... id: int | None = None name: str age: int | None = Field(default=None) secret_name: str - def __init__(self: Self, *, id: int | None = ..., name: str, age: int | None = ..., secret_name: str) -> None: ... - """) - - -# XXX: Delete this test once above passes -def test_dataclass_like_1_temp(): - tgt = eval_typing(Hero) - fmt = format_helper.format_class(tgt) - - assert fmt == textwrap.dedent("""\ - class Hero: @classmethod def __init_subclass__[T](cls: type[T]) -> typemap.typing.UpdateClass[InitFnType[T]]: ... def __init__(self: Self, *, id: int | None = ..., name: str, age: int | None = ..., secret_name: str) -> None: ... diff --git a/tests/test_type_eval.py b/tests/test_type_eval.py index 4926d36..c7d52fd 100644 --- a/tests/test_type_eval.py +++ b/tests/test_type_eval.py @@ -1927,19 +1927,20 @@ def __init_subclass__( def f(self) -> int: ... class B(A): - b0: int # omitted + b0: int # kept b1: int # overridden # b2 added in UpdateClass - def g(self) -> int: ... # omitted + def g(self) -> int: ... # kept - # Attrs + # Attrs: UpdateClass members first (a2, b1, b2), then non-overridden (b0) attrs = eval_typing(Attrs[B]) assert attrs.__args__ == ( Member[Literal["a1"], int, Never, Never, A], Member[Literal["a2"], str, Never, Never, B], Member[Literal["b1"], str, Never, Never, B], Member[Literal["b2"], str, Never, Never, B], + Member[Literal["b0"], int, Never, Never, B], ) # Members @@ -1951,6 +1952,7 @@ def g(self) -> int: ... # omitted Member[Literal["a2"], str, Never, Never, B], Member[Literal["b1"], str, Never, Never, B], Member[Literal["b2"], str, Never, Never, B], + Member[Literal["b0"], int, Never, Never, B], Member[ Literal["__init_subclass__"], classmethod[ @@ -1973,6 +1975,13 @@ def g(self) -> int: ... # omitted object, A, ], + Member[ + Literal["g"], + Callable[Params[Param[Literal["self"], B]], int], + Literal["ClassVar"], + object, + B, + ], ] ) @@ -1983,12 +1992,12 @@ def g(self) -> int: ... # omitted GetMember[B, Literal["a2"]], GetMember[B, Literal["b1"]], GetMember[B, Literal["b2"]], + GetMember[B, Literal["b0"]], GetMember[B, Literal["__init_subclass__"]], GetMember[B, Literal["f"]], + GetMember[B, Literal["g"]], ] ) - m = eval_typing(GetMember[B, Literal["g"]]) - assert m == Never type MembersExceptInitSubclass[T] = tuple[ @@ -2018,13 +2027,13 @@ def __init_subclass__[T]( def f(self) -> int: ... class B(A): - b0: int # omitted + b0: int # kept b1: int # overridden # b2 added in UpdateClass - def g(self) -> int: ... # omitted + def g(self) -> int: ... # kept - # Attrs + # Attrs: UpdateClass members first (a2, b1, b2), then non-overridden (b0) attrs = eval_typing(Attrs[B]) assert ( attrs @@ -2033,6 +2042,7 @@ def g(self) -> int: ... # omitted Member[Literal["a2"], str, Never, Never, B], Member[Literal["b1"], str, Never, Never, B], Member[Literal["b2"], str, Never, Never, B], + Member[Literal["b0"], int, Never, Never, B], ] ) @@ -2045,6 +2055,7 @@ def g(self) -> int: ... # omitted Member[Literal["a2"], str, Never, Never, B], Member[Literal["b1"], str, Never, Never, B], Member[Literal["b2"], str, Never, Never, B], + Member[Literal["b0"], int, Never, Never, B], Member[ Literal["f"], Callable[Params[Param[Literal["self"], A]], int], @@ -2052,6 +2063,13 @@ def g(self) -> int: ... # omitted object, A, ], + Member[ + Literal["g"], + Callable[Params[Param[Literal["self"], B]], int], + Literal["ClassVar"], + object, + B, + ], ] ) @@ -2062,11 +2080,11 @@ def g(self) -> int: ... # omitted GetMember[B, Literal["a2"]], GetMember[B, Literal["b1"]], GetMember[B, Literal["b2"]], + GetMember[B, Literal["b0"]], GetMember[B, Literal["f"]], + GetMember[B, Literal["g"]], ] ) - m = eval_typing(GetMember[B, Literal["g"]]) - assert m == Never type AttrsAsSets[T] = UpdateClass[ @@ -2089,7 +2107,7 @@ def f(self) -> int: ... class B(A): b: str - def g(self) -> int: ... # omitted + def g(self) -> int: ... # kept # Attrs attrs = eval_typing(Attrs[B]) @@ -2115,6 +2133,13 @@ def g(self) -> int: ... # omitted object, A, ], + Member[ + Literal["g"], + Callable[Params[Param[Literal["self"], B]], int], + Literal["ClassVar"], + object, + B, + ], ] ) @@ -2124,10 +2149,9 @@ def g(self) -> int: ... # omitted GetMember[B, Literal["a"]], GetMember[B, Literal["b"]], GetMember[B, Literal["f"]], + GetMember[B, Literal["g"]], ] ) - m = eval_typing(GetMember[B, Literal["g"]]) - assert m == Never def test_update_class_members_04(): @@ -2581,7 +2605,39 @@ class B(A): b: int attrs = eval_typing(Attrs[B]) - assert attrs == tuple[Member[Literal["a"], int, Never, Never, A]] + assert ( + attrs + == tuple[ + Member[Literal["a"], int, Never, Never, A], + Member[Literal["b"], int, Never, Never, B], + ] + ) + + +def test_update_class_never_removes(): + # A member with type Never in UpdateClass removes it + class A: + a: int + b: str + c: float + + def __init_subclass__[T]( + cls: type[T], + ) -> UpdateClass[Member[Literal["b"], Never],]: + super().__init_subclass__() + + class B(A): + d: bool + + attrs = eval_typing(Attrs[B]) + assert ( + attrs + == tuple[ + Member[Literal["a"], int, Never, Never, A], + Member[Literal["c"], float, Never, Never, A], + Member[Literal["d"], bool, Never, Never, B], + ] + ) ############## diff --git a/typemap/type_eval/_eval_operators.py b/typemap/type_eval/_eval_operators.py index 2618505..48f1bd2 100644 --- a/typemap/type_eval/_eval_operators.py +++ b/typemap/type_eval/_eval_operators.py @@ -170,7 +170,8 @@ def get_annotated_type_hints(cls, *, ctx, attrs_only=False, **kwargs): hints[k] = ty, tuple(sorted(quals)), init, acls - return hints + # A type of Never in UpdateClass removes the member + return {k: v for k, v in hints.items() if v[0] is not typing.Never} def get_annotated_method_hints(cls, *, ctx): @@ -311,14 +312,18 @@ def _create_updated_class( # Copy the module dct["__module__"] = t.__module__ - # Process the new members from UpdateClass + # Process UpdateClass members first to establish their ordering, + # then append non-overridden existing members. dct["__annotations__"] = annos = {} + update_names: set[str] = set() + for m in ms: tname, typ, quals, init, _ = typing.get_args(m) member_name = _eval_literal(tname, ctx) typ = _eval_types(typ, ctx) tquals = _eval_types(quals, ctx) + update_names.add(member_name) if ( type_eval.issubtype(typing.Literal["ClassVar"], tquals) and _is_method_like(typ) @@ -330,6 +335,21 @@ def _create_updated_class( annos[member_name] = _add_quals(typ, tquals) _unpack_init(dct, member_name, init) + # Append non-overridden existing annotations (preserving their order) + for name, typ in getattr(t, '__annotations__', {}).items(): + if name not in update_names: + annos[name] = typ + + # Append non-overridden existing methods and annotation defaults + existing_annos = getattr(t, '__annotations__', {}) + for name, value in t.__dict__.items(): + if name in update_names or name in _apply_generic.EXCLUDED_ATTRIBUTES: + continue + if isinstance(inspect.unwrap(value), types.FunctionType): + dct[name] = value + elif name in existing_annos: + dct[name] = value + # Create the updated class # If typing.Generic is a base, we need to use it with the type params