Skip to content

Commit e0cb559

Browse files
committed
Fix UpdateClass to preserve non-overridden existing members
UpdateClass was dropping all members declared in the current class that weren't explicitly returned by UpdateClass. Now returned members override existing ones, but existing annotations, methods, and defaults are preserved if not overridden. To remove a member, make its type Never.
1 parent 6b83967 commit e0cb559

File tree

3 files changed

+89
-32
lines changed

3 files changed

+89
-32
lines changed

tests/test_dataclass_like.py

Lines changed: 1 addition & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,6 @@
88

99
import typemap_extensions as typing
1010

11-
import pytest
12-
1311

1412
class FieldArgs(TypedDict, total=False):
1513
default: ReadOnly[object]
@@ -87,6 +85,7 @@ class Field[T: FieldArgs](typing.InitField[T]):
8785
"""
8886

8987

88+
# TODO: what could we do to make this work at runtime?
9089
def dataclass_ish[T](
9190
cls: type[T],
9291
) -> typing.UpdateClass[
@@ -133,30 +132,16 @@ class Hero(Model):
133132
from typemap.type_eval import format_helper
134133

135134

136-
@pytest.mark.xfail(reason="UpateClass currently drops things")
137135
def test_dataclass_like_1():
138136
tgt = eval_typing(Hero)
139137
fmt = format_helper.format_class(tgt)
140138

141139
assert fmt == textwrap.dedent("""\
142140
class Hero:
143-
@classmethod
144-
def __init_subclass__[T](cls: type[T]) -> typemap.typing.UpdateClass[InitFnType[T]]: ...
145141
id: int | None = None
146142
name: str
147143
age: int | None = Field(default=None)
148144
secret_name: str
149-
def __init__(self: Self, *, id: int | None = ..., name: str, age: int | None = ..., secret_name: str) -> None: ...
150-
""")
151-
152-
153-
# XXX: Delete this test once above passes
154-
def test_dataclass_like_1_temp():
155-
tgt = eval_typing(Hero)
156-
fmt = format_helper.format_class(tgt)
157-
158-
assert fmt == textwrap.dedent("""\
159-
class Hero:
160145
@classmethod
161146
def __init_subclass__[T](cls: type[T]) -> typemap.typing.UpdateClass[InitFnType[T]]: ...
162147
def __init__(self: Self, *, id: int | None = ..., name: str, age: int | None = ..., secret_name: str) -> None: ...

tests/test_type_eval.py

Lines changed: 66 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1927,19 +1927,20 @@ def __init_subclass__(
19271927
def f(self) -> int: ...
19281928

19291929
class B(A):
1930-
b0: int # omitted
1930+
b0: int # kept
19311931
b1: int # overridden
19321932
# b2 added in UpdateClass
19331933

1934-
def g(self) -> int: ... # omitted
1934+
def g(self) -> int: ... # kept
19351935

1936-
# Attrs
1936+
# Attrs: UpdateClass members first (a2, b1, b2), then non-overridden (b0)
19371937
attrs = eval_typing(Attrs[B])
19381938
assert attrs.__args__ == (
19391939
Member[Literal["a1"], int, Never, Never, A],
19401940
Member[Literal["a2"], str, Never, Never, B],
19411941
Member[Literal["b1"], str, Never, Never, B],
19421942
Member[Literal["b2"], str, Never, Never, B],
1943+
Member[Literal["b0"], int, Never, Never, B],
19431944
)
19441945

19451946
# Members
@@ -1951,6 +1952,7 @@ def g(self) -> int: ... # omitted
19511952
Member[Literal["a2"], str, Never, Never, B],
19521953
Member[Literal["b1"], str, Never, Never, B],
19531954
Member[Literal["b2"], str, Never, Never, B],
1955+
Member[Literal["b0"], int, Never, Never, B],
19541956
Member[
19551957
Literal["__init_subclass__"],
19561958
classmethod[
@@ -1973,6 +1975,13 @@ def g(self) -> int: ... # omitted
19731975
object,
19741976
A,
19751977
],
1978+
Member[
1979+
Literal["g"],
1980+
Callable[Params[Param[Literal["self"], B]], int],
1981+
Literal["ClassVar"],
1982+
object,
1983+
B,
1984+
],
19761985
]
19771986
)
19781987

@@ -1983,12 +1992,12 @@ def g(self) -> int: ... # omitted
19831992
GetMember[B, Literal["a2"]],
19841993
GetMember[B, Literal["b1"]],
19851994
GetMember[B, Literal["b2"]],
1995+
GetMember[B, Literal["b0"]],
19861996
GetMember[B, Literal["__init_subclass__"]],
19871997
GetMember[B, Literal["f"]],
1998+
GetMember[B, Literal["g"]],
19881999
]
19892000
)
1990-
m = eval_typing(GetMember[B, Literal["g"]])
1991-
assert m == Never
19922001

19932002

19942003
type MembersExceptInitSubclass[T] = tuple[
@@ -2018,13 +2027,13 @@ def __init_subclass__[T](
20182027
def f(self) -> int: ...
20192028

20202029
class B(A):
2021-
b0: int # omitted
2030+
b0: int # kept
20222031
b1: int # overridden
20232032
# b2 added in UpdateClass
20242033

2025-
def g(self) -> int: ... # omitted
2034+
def g(self) -> int: ... # kept
20262035

2027-
# Attrs
2036+
# Attrs: UpdateClass members first (a2, b1, b2), then non-overridden (b0)
20282037
attrs = eval_typing(Attrs[B])
20292038
assert (
20302039
attrs
@@ -2033,6 +2042,7 @@ def g(self) -> int: ... # omitted
20332042
Member[Literal["a2"], str, Never, Never, B],
20342043
Member[Literal["b1"], str, Never, Never, B],
20352044
Member[Literal["b2"], str, Never, Never, B],
2045+
Member[Literal["b0"], int, Never, Never, B],
20362046
]
20372047
)
20382048

@@ -2045,13 +2055,21 @@ def g(self) -> int: ... # omitted
20452055
Member[Literal["a2"], str, Never, Never, B],
20462056
Member[Literal["b1"], str, Never, Never, B],
20472057
Member[Literal["b2"], str, Never, Never, B],
2058+
Member[Literal["b0"], int, Never, Never, B],
20482059
Member[
20492060
Literal["f"],
20502061
Callable[Params[Param[Literal["self"], A]], int],
20512062
Literal["ClassVar"],
20522063
object,
20532064
A,
20542065
],
2066+
Member[
2067+
Literal["g"],
2068+
Callable[Params[Param[Literal["self"], B]], int],
2069+
Literal["ClassVar"],
2070+
object,
2071+
B,
2072+
],
20552073
]
20562074
)
20572075

@@ -2062,11 +2080,11 @@ def g(self) -> int: ... # omitted
20622080
GetMember[B, Literal["a2"]],
20632081
GetMember[B, Literal["b1"]],
20642082
GetMember[B, Literal["b2"]],
2083+
GetMember[B, Literal["b0"]],
20652084
GetMember[B, Literal["f"]],
2085+
GetMember[B, Literal["g"]],
20662086
]
20672087
)
2068-
m = eval_typing(GetMember[B, Literal["g"]])
2069-
assert m == Never
20702088

20712089

20722090
type AttrsAsSets[T] = UpdateClass[
@@ -2089,7 +2107,7 @@ def f(self) -> int: ...
20892107
class B(A):
20902108
b: str
20912109

2092-
def g(self) -> int: ... # omitted
2110+
def g(self) -> int: ... # kept
20932111

20942112
# Attrs
20952113
attrs = eval_typing(Attrs[B])
@@ -2115,6 +2133,13 @@ def g(self) -> int: ... # omitted
21152133
object,
21162134
A,
21172135
],
2136+
Member[
2137+
Literal["g"],
2138+
Callable[Params[Param[Literal["self"], B]], int],
2139+
Literal["ClassVar"],
2140+
object,
2141+
B,
2142+
],
21182143
]
21192144
)
21202145

@@ -2124,10 +2149,9 @@ def g(self) -> int: ... # omitted
21242149
GetMember[B, Literal["a"]],
21252150
GetMember[B, Literal["b"]],
21262151
GetMember[B, Literal["f"]],
2152+
GetMember[B, Literal["g"]],
21272153
]
21282154
)
2129-
m = eval_typing(GetMember[B, Literal["g"]])
2130-
assert m == Never
21312155

21322156

21332157
def test_update_class_members_04():
@@ -2581,7 +2605,35 @@ class B(A):
25812605
b: int
25822606

25832607
attrs = eval_typing(Attrs[B])
2584-
assert attrs == tuple[Member[Literal["a"], int, Never, Never, A]]
2608+
assert attrs == tuple[
2609+
Member[Literal["a"], int, Never, Never, A],
2610+
Member[Literal["b"], int, Never, Never, B],
2611+
]
2612+
2613+
2614+
def test_update_class_never_removes():
2615+
# A member with type Never in UpdateClass removes it
2616+
class A:
2617+
a: int
2618+
b: str
2619+
c: float
2620+
2621+
def __init_subclass__[T](
2622+
cls: type[T],
2623+
) -> UpdateClass[
2624+
Member[Literal["b"], Never],
2625+
]:
2626+
super().__init_subclass__()
2627+
2628+
class B(A):
2629+
d: bool
2630+
2631+
attrs = eval_typing(Attrs[B])
2632+
assert attrs == tuple[
2633+
Member[Literal["a"], int, Never, Never, A],
2634+
Member[Literal["c"], float, Never, Never, A],
2635+
Member[Literal["d"], bool, Never, Never, B],
2636+
]
25852637

25862638

25872639
##############

typemap/type_eval/_eval_operators.py

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,8 @@ def get_annotated_type_hints(cls, *, ctx, attrs_only=False, **kwargs):
170170

171171
hints[k] = ty, tuple(sorted(quals)), init, acls
172172

173-
return hints
173+
# A type of Never in UpdateClass removes the member
174+
return {k: v for k, v in hints.items() if v[0] is not typing.Never}
174175

175176

176177
def get_annotated_method_hints(cls, *, ctx):
@@ -311,14 +312,18 @@ def _create_updated_class(
311312
# Copy the module
312313
dct["__module__"] = t.__module__
313314

314-
# Process the new members from UpdateClass
315+
# Process UpdateClass members first to establish their ordering,
316+
# then append non-overridden existing members.
315317
dct["__annotations__"] = annos = {}
318+
update_names: set[str] = set()
319+
316320
for m in ms:
317321
tname, typ, quals, init, _ = typing.get_args(m)
318322
member_name = _eval_literal(tname, ctx)
319323
typ = _eval_types(typ, ctx)
320324
tquals = _eval_types(quals, ctx)
321325

326+
update_names.add(member_name)
322327
if (
323328
type_eval.issubtype(typing.Literal["ClassVar"], tquals)
324329
and _is_method_like(typ)
@@ -330,6 +335,21 @@ def _create_updated_class(
330335
annos[member_name] = _add_quals(typ, tquals)
331336
_unpack_init(dct, member_name, init)
332337

338+
# Append non-overridden existing annotations (preserving their order)
339+
for name, typ in getattr(t, '__annotations__', {}).items():
340+
if name not in update_names:
341+
annos[name] = typ
342+
343+
# Append non-overridden existing methods and annotation defaults
344+
existing_annos = getattr(t, '__annotations__', {})
345+
for name, value in t.__dict__.items():
346+
if name in update_names or name in _apply_generic.EXCLUDED_ATTRIBUTES:
347+
continue
348+
if isinstance(inspect.unwrap(value), types.FunctionType):
349+
dct[name] = value
350+
elif name in existing_annos:
351+
dct[name] = value
352+
333353
# Create the updated class
334354

335355
# If typing.Generic is a base, we need to use it with the type params

0 commit comments

Comments
 (0)