Skip to content

Commit e34d4ba

Browse files
committed
Do a better job substituting into types
1 parent 5442926 commit e34d4ba

5 files changed

Lines changed: 54 additions & 15 deletions

File tree

tests/test_type_dir.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,27 @@ def test_type_dir_7():
286286
assert d is int
287287

288288

289+
class Simple[T]:
290+
simple: T
291+
292+
293+
class Funny[T](Simple[list[T]]):
294+
pass
295+
296+
297+
class Funny2(Funny[int]):
298+
pass
299+
300+
301+
def test_type_dir_8():
302+
d = eval_typing(Funny2)
303+
304+
assert format_helper.format_class(d) == textwrap.dedent("""\
305+
class Funny2:
306+
simple: list[int]
307+
""")
308+
309+
289310
def _get_member(members, name):
290311
return next(
291312
iter(m for m in members.__args__ if m.__args__[0].__args__[0] == name)

typemap/type_eval/_apply_generic.py

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,11 @@
44
import types
55
import typing
66

7+
from typing import _GenericAlias as typing_GenericAlias # type: ignore [attr-defined] # noqa: PLC2701
8+
9+
710
from . import _eval_typing
11+
from . import _typing_inspect
812

913
if typing.TYPE_CHECKING:
1014
from typing import Any
@@ -44,8 +48,17 @@ def dump(self, *, _level: int = 0):
4448
b.dump(_level=_level + 1)
4549

4650

51+
def substitute(ty, args):
52+
if ty in args:
53+
return args[ty]
54+
elif isinstance(ty, (typing_GenericAlias, types.GenericAlias)):
55+
return ty.__origin__[*[substitute(t, args) for t in ty.__args__]]
56+
else:
57+
return ty
58+
59+
4760
def box(cls: type[Any]) -> Boxed:
48-
def _box(cls: type[Any], args: dict[str, Any]) -> Boxed:
61+
def _box(cls: type[Any], args: dict[Any, Any]) -> Boxed:
4962
boxed_bases: list[Boxed] = []
5063

5164
orig_bases = cls.__dict__.get("__orig_bases__")
@@ -60,14 +73,12 @@ def _box(cls: type[Any], args: dict[str, Any]) -> Boxed:
6073

6174
if issubclass(base, typing.Generic):
6275
if base_params := getattr(base, "__parameters__", None):
76+
# obase should be _GenericAlias...
6377
boxed_args = {}
6478
for param, arg in zip(
6579
base_params, obase.__args__, strict=True
6680
):
67-
if arg in args:
68-
boxed_args[param] = args[arg]
69-
else:
70-
boxed_args[param] = arg
81+
boxed_args[param] = substitute(arg, args)
7182
else:
7283
boxed_args = {}
7384

@@ -85,7 +96,7 @@ def _box(cls: type[Any], args: dict[str, Any]) -> Boxed:
8596
cls = cls.__origin__
8697
else:
8798
if params := getattr(cls, "__parameters__", None):
88-
args = {p: p for p in params}
99+
args = {p: _typing_inspect.param_default(p) for p in params}
89100
else:
90101
args = {}
91102

@@ -120,7 +131,7 @@ def merge_boxed_mro(seqs: list[list[Boxed]]) -> list[Boxed]:
120131

121132
def _compute_mro(C: Boxed) -> list[Boxed]:
122133
return merge_boxed_mro(
123-
[[C]] + list(map(_compute_mro, C.bases)) + [list(C.bases)]
134+
[[C]] + [_compute_mro(b) for b in C.bases] + [list(C.bases)]
124135
)
125136

126137

typemap/type_eval/_eval_operators.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -423,10 +423,7 @@ def _get_defaults(base_head):
423423
return (typing.Any, SpecialFormEllipsis)
424424

425425
if params := _get_params(base_head):
426-
return tuple(
427-
typing.Any if t.__default__ == typing.NoDefault else t.__default__
428-
for t in params
429-
)
426+
return tuple(_typing_inspect.param_default(p) for p in params)
430427

431428
return (typing.Any,) * arity
432429

typemap/type_eval/_eval_typing.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
import types
1010
import typing
1111

12-
from typing import _GenericAlias # type: ignore [attr-defined] # noqa: PLC2701
12+
from typing import _GenericAlias as typing_GenericAlias # type: ignore [attr-defined] # noqa: PLC2701
1313

1414

1515
if typing.TYPE_CHECKING:
@@ -282,7 +282,12 @@ def _eval_type_alias(obj: typing.TypeAliasType, ctx: EvalContext):
282282

283283

284284
@_eval_types_impl.register
285-
def _eval_types_generic(obj: types.GenericAlias, ctx: EvalContext):
285+
def _eval_applied_type_alias(obj: types.GenericAlias, ctx: EvalContext):
286+
"""Eval a types.GenericAlias -- typically an applied type alias
287+
288+
This is typically an application of a type alias... except it can
289+
also be an application of a built-in type (like list, tuple, dict)
290+
"""
286291
new_args = tuple(_eval_types(arg, ctx) for arg in obj.__args__)
287292

288293
new_obj = obj.__origin__[new_args] # type: ignore[index]
@@ -315,9 +320,10 @@ def _eval_types_generic(obj: types.GenericAlias, ctx: EvalContext):
315320

316321

317322
@_eval_types_impl.register
318-
def _eval_typing_generic(obj: _GenericAlias, ctx: EvalContext):
323+
def _eval_applied_class(obj: typing_GenericAlias, ctx: EvalContext):
324+
"""Eval a typing._GenericAlias -- an applied user-defined class"""
319325
# generic *classes* are typing._GenericAlias while generic type
320-
# aliases are # types.GenericAlias? Why in the world.
326+
# aliases are types.GenericAlias? Why in the world.
321327
if func := _eval_funcs.get(obj.__origin__):
322328
new_args = tuple(_eval_types(arg, ctx) for arg in obj.__args__)
323329
ret = func(*new_args, ctx=ctx)

typemap/type_eval/_typing_inspect.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,10 @@ def is_eval_proxy(t: Any) -> TypeGuard[type[_eval_typing._EvalProxy]]:
149149
return isinstance(t, type) and issubclass(t, _eval_typing._EvalProxy)
150150

151151

152+
def param_default(p) -> Any:
153+
return Any if p.__default__ == typing.NoDefault else p.__default__
154+
155+
152156
__all__ = (
153157
"is_annotated",
154158
"is_forward_ref",

0 commit comments

Comments
 (0)