Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 3 additions & 16 deletions tests/test_dataclass_like.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,6 @@

import typemap_extensions as typing

import pytest


class FieldArgs(TypedDict, total=False):
default: ReadOnly[object]
Expand Down Expand Up @@ -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__

"""
Expand Down Expand Up @@ -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: ...
Expand Down
84 changes: 70 additions & 14 deletions tests/test_type_eval.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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[
Expand All @@ -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,
],
]
)

Expand All @@ -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[
Expand Down Expand Up @@ -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
Expand All @@ -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],
]
)

Expand All @@ -2045,13 +2055,21 @@ 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],
Literal["ClassVar"],
object,
A,
],
Member[
Literal["g"],
Callable[Params[Param[Literal["self"], B]], int],
Literal["ClassVar"],
object,
B,
],
]
)

Expand All @@ -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[
Expand All @@ -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])
Expand All @@ -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,
],
]
)

Expand All @@ -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():
Expand Down Expand Up @@ -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],
]
)


##############
Expand Down
24 changes: 22 additions & 2 deletions typemap/type_eval/_eval_operators.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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)
Expand All @@ -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
Expand Down
Loading