Skip to content

Commit 84ef843

Browse files
authored
Support star unpacking of operators, aliases, and tuples (#70)
Aliases and tuples weren't working for a simple reason, which is that unpacking a types.GenericAlias with a `*` doesn't produce an `Unpack`, but produces a `types.GenericAlias` with `__unpacked__` set. Since we didn't consider that, we would simply lose track of it. Fix that by stripping __unpacked__, evaling the inner type, and adding an Unpack. Built-in operators didn't work because `_GenericAlias` produces an `_UnpackGenericAlias` when iterated, and `_UnpackGenericAlias` raises an exception when it is passed to a variadic generic if it's body is either sort of generic alias but isn't a tuple. Fix that by a bunch of annoying overloading of what aliases our operators produce. And then, with the annoying parts done, we just need to actually implement the unpacking of tuple types.
1 parent 9c44791 commit 84ef843

7 files changed

Lines changed: 94 additions & 63 deletions

File tree

pep.rst

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1093,11 +1093,6 @@ NumPy-style broadcasting
10931093
) -> Array[DType, *Broadcast[tuple[*Shape], tuple[*Shape2]]]:
10941094
raise BaseException
10951095

1096-
type AppendTuple[A, B] = tuple[
1097-
*[x for x in typing.Iter[A]],
1098-
B,
1099-
]
1100-
11011096
type MergeOne[T, S] = (
11021097
T
11031098
if typing.Matches[T, S] or typing.Matches[S, Literal[1]]
@@ -1118,8 +1113,9 @@ NumPy-style broadcasting
11181113
if typing.Bool[Empty[T]]
11191114
else T
11201115
if typing.Bool[Empty[S]]
1121-
else AppendTuple[
1122-
Broadcast[DropLast[T], DropLast[S]], MergeOne[Last[T], Last[S]]
1116+
else tuple[
1117+
*Broadcast[DropLast[T], DropLast[S]],
1118+
MergeOne[Last[T], Last[S]],
11231119
]
11241120
)
11251121

tests/test_fastapilike_1.py

Lines changed: 0 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -67,40 +67,12 @@ class _Default:
6767
],
6868
Literal["ClassVar"],
6969
]
70-
type AddInit[T] = NewProtocol[
71-
InitFnType[T],
72-
*[x for x in Iter[Members[T]]],
73-
]
74-
75-
"""TODO:
76-
77-
We would really like to instead write:
7870

7971
type AddInit[T] = NewProtocol[
8072
InitFnType[T],
8173
*Members[T],
8274
]
8375

84-
but we struggle here because typing wants to unpack the Members tuple
85-
itself. I'm not sure if there is a nice way to resolve this. We
86-
*could* make our consumers (NewProtocol etc) be more flexible about
87-
these things but I don't think that is right.
88-
89-
The frustrating thing is that it doesn't do much with the unpacked
90-
version, just some checks!
91-
92-
We could fix typing to allow it, and probably provide a hack around it
93-
in the mean time.
94-
95-
Lurr! Writing *this* gets past the typing checks (though we don't
96-
support it yet):
97-
98-
type AddInit[T] = NewProtocol[
99-
InitFnType[T],
100-
*tuple[*Members[T]],
101-
]
102-
"""
103-
10476
# Strip `| None` from a type by iterating over its union components
10577
# and filtering
10678
type NotOptional[T] = Union[

tests/test_nplike.py

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,6 @@ def __add__[*Shape2](
1414
raise BaseException
1515

1616

17-
type AppendTuple[A, B] = tuple[
18-
*[x for x in typing.Iter[A]],
19-
B,
20-
]
21-
2217
type MergeOne[T, S] = (
2318
T
2419
if typing.Matches[T, S] or typing.Matches[S, Literal[1]]
@@ -39,8 +34,9 @@ def __add__[*Shape2](
3934
if typing.Bool[Empty[T]]
4035
else T
4136
if typing.Bool[Empty[S]]
42-
else AppendTuple[
43-
Broadcast[DropLast[T], DropLast[S]], MergeOne[Last[T], Last[S]]
37+
else tuple[
38+
*Broadcast[DropLast[T], DropLast[S]],
39+
MergeOne[Last[T], Last[S]],
4440
]
4541
)
4642

@@ -49,11 +45,7 @@ def __add__[*Shape2](
4945
type GetElem[T] = typing.GetArg[T, Array, Literal[0]]
5046
type GetShape[T] = typing.Slice[typing.GetArgs[T, Array], Literal[1], None]
5147

52-
# type Apply[T, S] = Array[GetElem[T], *Broadcast[GetShape[T], GetShape[S]]]
53-
type Apply[T, S] = Array[
54-
GetElem[T],
55-
*[x for x in typing.Iter[Broadcast[GetShape[T], GetShape[S]]]],
56-
]
48+
type Apply[T, S] = Array[GetElem[T], *Broadcast[GetShape[T], GetShape[S]]]
5749

5850
######
5951
from typemap.type_eval import eval_typing, TypeMapError

tests/test_type_eval.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -74,10 +74,7 @@ class F_int(F[int]):
7474
if not IsSub[GetType[p], A]
7575
else Member[GetName[p], OrGotcha[MapRecursive[A]]]
7676
)
77-
# XXX: This next line *ought* to work, but we haven't
78-
# implemented it yet.
79-
# for p in Iter[*Attrs[A], *Attrs[F_int]]
80-
for p in Iter[ConcatTuples[Attrs[A], Attrs[F_int]]]
77+
for p in Iter[tuple[*Attrs[A], *Attrs[F_int]]]
8178
],
8279
Member[Literal["control"], float],
8380
]

typemap/type_eval/_eval_operators.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1088,7 +1088,8 @@ def _eval_NewProtocol(*etyps: Member, ctx):
10881088
dct: dict[str, object] = {}
10891089
dct["__annotations__"] = annos = {}
10901090

1091-
for tname, typ, quals, init, _ in (typing.get_args(prop) for prop in etyps):
1091+
members = [typing.get_args(prop) for prop in etyps]
1092+
for tname, typ, quals, init, _ in members:
10921093
name = _eval_literal(tname, ctx)
10931094
typ = _eval_types(typ, ctx)
10941095
tquals = _eval_types(quals, ctx)

typemap/type_eval/_eval_typing.py

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,12 @@
1515
_CallableGenericAlias as typing_CallableGenericAlias,
1616
_LiteralGenericAlias as typing_LiteralGenericAlias,
1717
_AnnotatedAlias as typing_AnnotatedAlias,
18+
_UnpackGenericAlias as typing_UnpackGenericAlias,
1819
)
1920

2021

2122
if typing.TYPE_CHECKING:
22-
from typing import Any
23+
from typing import Any, Sequence
2324

2425
from . import _apply_generic, _typing_inspect
2526

@@ -377,14 +378,38 @@ def _eval_type_alias(obj: typing.TypeAliasType, ctx: EvalContext):
377378
return _eval_types(unpacked, ctx)
378379

379380

381+
def _eval_args(args: Sequence[Any], ctx: EvalContext) -> tuple[Any]:
382+
evaled = []
383+
for arg in args:
384+
ev = _eval_types(arg, ctx)
385+
if isinstance(ev, typing_UnpackGenericAlias):
386+
if (args := ev.__typing_unpacked_tuple_args__) is not None:
387+
evaled.extend(args)
388+
else:
389+
evaled.append(ev)
390+
else:
391+
evaled.append(ev)
392+
return tuple(evaled)
393+
394+
380395
@_eval_types_impl.register
381396
def _eval_applied_type_alias(obj: types.GenericAlias, ctx: EvalContext):
382397
"""Eval a types.GenericAlias -- typically an applied type alias
383398
384399
This is typically an application of a type alias... except it can
385-
also be an application of a built-in type (like list, tuple, dict)
400+
also be an application of a built-in type (like list, tuple, dict).
401+
402+
It can *also* have an Unpack integrated with it, if __unpacked__ is set.
386403
"""
387-
new_args = tuple(_eval_types(arg, ctx) for arg in obj.__args__)
404+
405+
# If __unpacked__ is set, then we reconstruct a version without
406+
# __unpacked__ set and evaluate *that*. This centralizes the
407+
# unpacked handling and simplifies the cache situation.
408+
if obj.__unpacked__:
409+
stripped = _apply_type(obj.__origin__, obj.__args__)
410+
return typing.Unpack[_eval_types(stripped, ctx)]
411+
412+
new_args = _eval_args(obj.__args__, ctx)
388413

389414
new_obj = _apply_type(obj.__origin__, new_args)
390415
if isinstance(obj.__origin__, type):
@@ -433,7 +458,7 @@ def _eval_applied_class(obj: typing_GenericAlias, ctx: EvalContext):
433458
"""Eval a typing._GenericAlias -- an applied user-defined class"""
434459
# generic *classes* are typing._GenericAlias while generic type
435460
# aliases are types.GenericAlias? Why in the world.
436-
new_args = tuple(_eval_types(arg, ctx) for arg in typing.get_args(obj))
461+
new_args = _eval_args(typing.get_args(obj), ctx)
437462

438463
if func := _eval_funcs.get(obj.__origin__):
439464
ret = func(*new_args, ctx=ctx)

typemap/typing.py

Lines changed: 55 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,56 @@
11
import contextvars
22
import typing
3-
from typing import Literal
4-
from typing import _GenericAlias, _LiteralGenericAlias # type: ignore
3+
import types
4+
5+
from typing import Literal, Unpack
6+
from typing import _GenericAlias, _LiteralGenericAlias, _UnpackGenericAlias # type: ignore
57

68
_SpecialForm: typing.Any = typing._SpecialForm
79

10+
###
11+
12+
# Here is a bunch of annoying internals stuff!
13+
14+
15+
class _TupleLikeOperator:
16+
@classmethod
17+
def __class_getitem__(cls, args):
18+
# Return an _IterSafeGenericAlias instead of a _GenericAlias
19+
res = super().__class_getitem__(args)
20+
return _IterSafeGenericAlias(res.__origin__, res.__args__)
21+
22+
23+
# The base _GenericAlias has an __iter__ method that returns
24+
# Unpack[self], which blows up when it's passed to something and
25+
# doesn't have a tuple inside (because it hasn't been evaluated yet!).
26+
# So we make own _GenericAlias that makes our own _UnpackGenericAlias
27+
# that we make sure works.
28+
#
29+
# Probably these exact hacks will need to go into our
30+
# typing_extensions version of this, but for the typing version they
31+
# can get merged into real classes.
32+
class _IterSafeGenericAlias(_GenericAlias, _root=True): # type: ignore[call-arg]
33+
def __iter__(self):
34+
yield _IterSafeUnpackGenericAlias(origin=Unpack, args=(self,))
35+
36+
37+
class _IterSafeUnpackGenericAlias(_UnpackGenericAlias, _root=True): # type: ignore[call-arg]
38+
@property
39+
def __typing_unpacked_tuple_args__(self):
40+
# This is basically the same as in _UnpackGenericAlias except
41+
# we don't blow up if the origin isn't a tuple.
42+
assert self.__origin__ is Unpack
43+
assert len(self.__args__) == 1
44+
(arg,) = self.__args__
45+
if isinstance(arg, (_GenericAlias, types.GenericAlias)):
46+
if arg.__origin__ is tuple:
47+
return arg.__args__
48+
return None
49+
50+
51+
###
52+
53+
854
# Not type-level computation but related
955

1056

@@ -119,15 +165,15 @@ class Param[N: str | None, T, Q: ParamQuals = typing.Never]:
119165
type GetDefiner[T: Member] = GetMemberType[T, Literal["definer"]]
120166

121167

122-
class Attrs[T]:
168+
class Attrs[T](_TupleLikeOperator):
123169
pass
124170

125171

126-
class Members[T]:
172+
class Members[T](_TupleLikeOperator):
127173
pass
128174

129175

130-
class FromUnion[T]:
176+
class FromUnion[T](_TupleLikeOperator):
131177
pass
132178

133179

@@ -143,7 +189,7 @@ class GetArg[Tp, Base, Idx: int]:
143189
pass
144190

145191

146-
class GetArgs[Tp, Base]:
192+
class GetArgs[Tp, Base](_TupleLikeOperator):
147193
pass
148194

149195

@@ -155,7 +201,9 @@ class Length[S: tuple]:
155201
pass
156202

157203

158-
class Slice[S: str | tuple, Start: int | None, End: int | None]:
204+
class Slice[S: str | tuple, Start: int | None, End: int | None](
205+
_TupleLikeOperator
206+
):
159207
pass
160208

161209

0 commit comments

Comments
 (0)