diff --git a/pyproject.toml b/pyproject.toml index ee5d963..68d3803 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,7 +13,7 @@ include = ["typemap", "typemap.*", "typemap_extensions"] test = [ "pytest>=7.0", "ruff", - "mypy @ git+https://github.com/msullivan/mypy-typemap@fbc5d6c16834379307857318e6c32326b5d8a201", + "mypy @ git+https://github.com/msullivan/mypy-typemap@5250279d38109fedafff709488939c38901783bd", ] [tool.uv] diff --git a/tests/test_astlike_1.py b/tests/test_astlike_1.py index 957253b..ec13fe3 100644 --- a/tests/test_astlike_1.py +++ b/tests/test_astlike_1.py @@ -14,6 +14,7 @@ IsAssignable, Iter, IsEquivalent, + Map, Member, NewProtocol, RaiseError, @@ -46,7 +47,7 @@ type CombineVarArgs[Ls: tuple[VarArg], Rs: tuple[VarArg]] = tuple[ - *[ + *Map( VarArg[ VarArgName[x], ( @@ -56,7 +57,7 @@ ) else GetArg[ # Common to both Ls and Rs tuple[ - *[ + *Map( ( VarArgType[x] if IsAssignable[VarArgType[x], VarArgType[y]] @@ -73,7 +74,7 @@ ) for y in Iter[Rs] if IsEquivalent[VarArgName[x], VarArgName[y]] - ] + ) ], tuple, typing.Literal[0], @@ -81,14 +82,14 @@ ), ] for x in Iter[Ls] - ], - *[ # Unique to Rs + ), + *Map( # Unique to Rs x for x in Iter[Rs] if not any( # Unique to Rs IsEquivalent[VarArgName[x], VarArgName[y]] for y in Iter[Ls] ) - ], + ), ] diff --git a/tests/test_call.py b/tests/test_call.py index e730dca..3b0e91f 100644 --- a/tests/test_call.py +++ b/tests/test_call.py @@ -7,6 +7,7 @@ Attrs, BaseTypedDict, NewProtocol, + Map, Member, Iter, ) @@ -17,7 +18,7 @@ def func[*T, K: BaseTypedDict]( *args: Unpack[T], **kwargs: Unpack[K], -) -> NewProtocol[*[Member[c.name, int] for c in Iter[Attrs[K]]]]: +) -> NewProtocol[*Map(Member[c.name, int] for c in Iter[Attrs[K]])]: raise NotImplementedError diff --git a/tests/test_dataclass_like.py b/tests/test_dataclass_like.py index 36a66bf..cd6aaf7 100644 --- a/tests/test_dataclass_like.py +++ b/tests/test_dataclass_like.py @@ -65,7 +65,7 @@ def _check_hero_init() -> None: Callable[ typing.Params[ typing.Param[Literal["self"], T], - *[ + *typing.Map( typing.Param[ p.name, p.type, @@ -79,7 +79,7 @@ def _check_hero_init() -> None: else Literal["keyword", "default"], ] for p in typing.Iter[typing.Attrs[T]] - ], + ), ], None, ], @@ -87,7 +87,7 @@ def _check_hero_init() -> None: ] type AddInit[T] = typing.NewProtocol[ InitFnType[T], - *[x for x in typing.Iter[typing.Members[T]]], + *typing.Map(x for x in typing.Iter[typing.Members[T]]), ] """ diff --git a/tests/test_eval_call_with_types.py b/tests/test_eval_call_with_types.py index 1c953b8..534bea2 100644 --- a/tests/test_eval_call_with_types.py +++ b/tests/test_eval_call_with_types.py @@ -10,6 +10,7 @@ GetArg, IsAssignable, Iter, + Map, Members, Param, Params, @@ -287,7 +288,7 @@ def func[T](x: C[T]) -> T: ... type GetCallableMember[T, N: str] = GetArg[ tuple[ - *[ + *Map( m.type for m in Iter[Members[T]] if ( @@ -295,7 +296,7 @@ def func[T](x: C[T]) -> T: ... or IsAssignable[m.type, GenericCallable] ) and IsAssignable[m.name, N] - ] + ) ], tuple, Literal[0], diff --git a/tests/test_fastapilike_1.py b/tests/test_fastapilike_1.py index 3456f23..b3ac9ef 100644 --- a/tests/test_fastapilike_1.py +++ b/tests/test_fastapilike_1.py @@ -14,6 +14,7 @@ from typemap_extensions import ( NewProtocol, Iter, + Map, Attrs, IsAssignable, GetAnnotations, @@ -53,7 +54,7 @@ class _Default: Callable[ Params[ Param[Literal["self"], Self], - *[ + *Map( Param[ p.name, DropAnnotations[p.type], @@ -65,7 +66,7 @@ class _Default: else Literal["keyword"], ] for p in Iter[Attrs[T]] - ], + ), ], None, ], @@ -76,13 +77,13 @@ class _Default: InitFnType[T], # TODO: mypy rejects this -- should it work? # *Members[T], - *[t for t in Iter[Members[T]]], + *Map(t for t in Iter[Members[T]]), ] # Strip `| None` from a type by iterating over its union components # and filtering type NotOptional[T] = Union[ - *[x for x in Iter[FromUnion[T]] if not IsAssignable[x, None]] + *Map(x for x in Iter[FromUnion[T]] if not IsAssignable[x, None]) ] # Adjust an attribute type for use in Public below by dropping | None for @@ -97,27 +98,27 @@ class _Default: # Drop all the annotations, since this is for data getting returned to users # from the DB, so we don't need default values. type Public[T] = NewProtocol[ - *[ + *Map( Member[p.name, FixPublicType[p.type], p.quals] for p in Iter[Attrs[T]] if not IsAssignable[Literal[PropQuals.HIDDEN], GetAnnotations[p.type]] - ] + ) ] # Create takes everything but the primary key and preserves defaults type Create[T] = NewProtocol[ - *[ + *Map( Member[p.name, p.type, p.quals] for p in Iter[Attrs[T]] if not IsAssignable[Literal[PropQuals.PRIMARY], GetAnnotations[p.type]] - ] + ) ] # Update takes everything but the primary key, but makes them all have # None defaults type Update[T] = NewProtocol[ - *[ + *Map( Member[ p.name, HasDefault[DropAnnotations[p.type] | None, None], @@ -125,7 +126,7 @@ class _Default: ] for p in Iter[Attrs[T]] if not IsAssignable[Literal[PropQuals.PRIMARY], GetAnnotations[p.type]] - ] + ) ] diff --git a/tests/test_fastapilike_2.py b/tests/test_fastapilike_2.py index 6913e05..94cb636 100644 --- a/tests/test_fastapilike_2.py +++ b/tests/test_fastapilike_2.py @@ -37,11 +37,11 @@ class Field[T: FieldArgs](typing.InitField[T]): # Strip `| None` from a type by iterating over its union components # and filtering type NotOptional[T] = Union[ - *[ + *typing.Map( x for x in typing.Iter[typing.FromUnion[T]] if not typing.IsAssignable[x, None] - ] + ) ] # Adjust an attribute type for use in Public below by dropping | None for @@ -58,7 +58,7 @@ class Field[T: FieldArgs](typing.InitField[T]): # Drop all the annotations, since this is for data getting returned to users # from the DB, so we don't need default values. type Public[T] = typing.NewProtocol[ - *[ + *typing.Map( typing.Member[ p.name, FixPublicType[p.type, p.init], @@ -68,7 +68,7 @@ class Field[T: FieldArgs](typing.InitField[T]): if not typing.IsAssignable[ Literal[True], GetFieldItem[p.init, Literal["hidden"]] ] - ] + ) ] # Begin PEP section: Automatically deriving FastAPI CRUD models @@ -88,7 +88,7 @@ class Field[T: FieldArgs](typing.InitField[T]): # Create takes everything but the primary key and preserves defaults type Create[T] = typing.NewProtocol[ - *[ + *typing.Map( typing.Member[ p.name, p.type, @@ -100,7 +100,7 @@ class Field[T: FieldArgs](typing.InitField[T]): Literal[True], GetFieldItem[p.init, Literal["primary_key"]], ] - ] + ) ] """ @@ -122,7 +122,7 @@ class Field[T: FieldArgs](typing.InitField[T]): # Update takes everything but the primary key, but makes them all have # None defaults type Update[T] = typing.NewProtocol[ - *[ + *typing.Map( typing.Member[ p.name, p.type | None, @@ -134,7 +134,7 @@ class Field[T: FieldArgs](typing.InitField[T]): Literal[True], GetFieldItem[p.init, Literal["primary_key"]], ] - ] + ) ] ## @@ -145,7 +145,7 @@ class Field[T: FieldArgs](typing.InitField[T]): Callable[ typing.Params[ typing.Param[Literal["self"], Self], # type: ignore[misc] - *[ + *typing.Map( typing.Param[ p.name, p.type, @@ -159,7 +159,7 @@ class Field[T: FieldArgs](typing.InitField[T]): else Literal["keyword", "default"], ] for p in typing.Iter[typing.Attrs[T]] - ], + ), ], None, ], @@ -167,7 +167,7 @@ class Field[T: FieldArgs](typing.InitField[T]): ] type AddInit[T] = typing.NewProtocol[ InitFnType[T], - *[x for x in typing.Iter[typing.Members[T]]], + *typing.Map(x for x in typing.Iter[typing.Members[T]]), ] diff --git a/tests/test_qblike.py b/tests/test_qblike.py index 3fb088c..1665659 100644 --- a/tests/test_qblike.py +++ b/tests/test_qblike.py @@ -11,6 +11,7 @@ BaseTypedDict, NewProtocol, Iter, + Map, Attrs, IsAssignable, Member, @@ -30,7 +31,7 @@ class Link[T]: type PropsOnly[T] = NewProtocol[ - *[p for p in Iter[Attrs[T]] if IsAssignable[p.type, Property]] + *Map(p for p in Iter[Attrs[T]] if IsAssignable[p.type, Property]) ] # Conditional type alias! @@ -44,13 +45,13 @@ def select[K: BaseTypedDict]( /, **kwargs: Unpack[K], ) -> NewProtocol[ - *[ + *Map( Member[ c.name, FilterLinks[GetMemberType[A, c.name]], ] for c in Iter[Attrs[K]] - ] + ) ]: raise NotImplementedError diff --git a/tests/test_qblike_2.py b/tests/test_qblike_2.py index 67a8c45..edddac3 100644 --- a/tests/test_qblike_2.py +++ b/tests/test_qblike_2.py @@ -58,13 +58,13 @@ def select[ModelT, K: typing.BaseTypedDict]( **kwargs: Unpack[K], ) -> list[ typing.NewProtocol[ - *[ + *typing.Map( typing.Member[ c.name, ConvertField[typing.GetMemberType[ModelT, c.name]], ] for c in typing.Iter[typing.Attrs[K]] - ] + ) ] ]: raise NotImplementedError @@ -112,11 +112,11 @@ def select[ModelT, K: typing.BaseTypedDict]( """ type PropsOnly[T] = typing.NewProtocol[ - *[ + *typing.Map( typing.Member[p.name, PointerArg[p.type]] for p in typing.Iter[typing.Attrs[T]] if typing.IsAssignable[p.type, Property] - ] + ) ] """ diff --git a/tests/test_qblike_3.py b/tests/test_qblike_3.py index b6cea25..50162ac 100644 --- a/tests/test_qblike_3.py +++ b/tests/test_qblike_3.py @@ -27,6 +27,7 @@ IsAssignable, Iter, IsEquivalent, + Map, Member, Members, NewProtocol, @@ -139,7 +140,7 @@ class Table[name: str]: def __init_subclass__[T]( cls: type[T], ) -> UpdateClass[ - *[ + *Map( Member[ m.name, _Field[ @@ -152,8 +153,8 @@ def __init_subclass__[T]( ] for m in Iter[Members[T]] if IsAssignable[m.type, Field] - ], - *[m for m in Iter[Members[T]] if not IsAssignable[m.type, Field]], + ), + *Map(m for m in Iter[Members[T]] if not IsAssignable[m.type, Field]), ]: super().__init_subclass__() @@ -241,11 +242,11 @@ class DbLinkSource[Args: DbLinkSourceArgs](InitField[Args]): type EntryFields[E: QueryEntry] = GetArg[E, tuple, Literal[1]] type EntryFieldMembers[T: Table, FieldNames: tuple[Literal[str], ...]] = tuple[ - *[ + *Map( m for m in Iter[Attrs[T]] if any(IsAssignable[m.name, f] for f in Iter[FieldNames]) - ] + ) ] type EntryIsTable[E: QueryEntry, T: Table] = IsEquivalent[EntryTable[E], T] @@ -255,7 +256,9 @@ class DbLinkSource[Args: DbLinkSourceArgs](InitField[Args]): type MakeQueryEntryAllFields[T: Table] = QueryEntry[ T, - tuple[*[m.name for m in Iter[Attrs[T]] if IsAssignable[m.type, _Field]],], + tuple[ + *Map(m.name for m in Iter[Attrs[T]] if IsAssignable[m.type, _Field]), + ], ] type MakeQueryEntryNamedFields[ T: Table, @@ -263,22 +266,22 @@ class DbLinkSource[Args: DbLinkSourceArgs](InitField[Args]): ] = QueryEntry[ T, tuple[ - *[ + *Map( m.name for m in Iter[Attrs[T]] if IsAssignable[m.type, _Field] and any( IsAssignable[FieldName[m.type], f] for f in Iter[FieldNames] ) - ], + ), ], ] type AddTable[Entries, New: Table] = tuple[ - *[ # Existing entries + *Map( # Existing entries (e if not Bool[EntryIsTable[e, New]] else MakeQueryEntryAllFields[New]) for e in Iter[Entries] - ], + ), *( # Add entries if not present [] if Bool[EntriesHasTable[Entries, New]] @@ -286,17 +289,17 @@ class DbLinkSource[Args: DbLinkSourceArgs](InitField[Args]): ), ] type AddField[Entries, New: _Field] = tuple[ - *[ # Existing entries + *Map( # Existing entries ( e # Non-matching entry if not Bool[EntryIsTable[e, FieldTable[New]]] else MakeQueryEntryNamedFields[ EntryTable[e], - tuple[*[f for f in Iter[EntryFields[e]]], FieldName[New]], + tuple[*Map(f for f in Iter[EntryFields[e]]), FieldName[New]], ] ) for e in Iter[Entries] - ], + ), *( # Add entries if not present e for e in Iter[tuple[QueryEntry[FieldTable[New], tuple[FieldName[New]]]]] @@ -326,7 +329,7 @@ class Query[Es: tuple[QueryEntry[Table, tuple[Member]], ...]]: type Select[T: Table, FieldNames: tuple[Literal[str], ...]] = NewProtocol[ - *[ + *Map( Member[ m.name, ( @@ -336,7 +339,7 @@ class Query[Es: tuple[QueryEntry[Table, tuple[Member]], ...]]: ), ] for m in Iter[EntryFieldMembers[T, FieldNames]] - ], + ), ] @@ -347,13 +350,13 @@ class Query[Es: tuple[QueryEntry[Table, tuple[Member]], ...]]: ] if IsAssignable[Literal[1], Length[Es]] else NewProtocol[ - *[ + *Map( Member[ GetSpecialAttr[EntryTable[e], Literal["__name__"]], Select[EntryTable[e], EntryFields[e]], ] for e in Iter[Es] - ] + ) ] ) @@ -413,7 +416,7 @@ class Comment(Table[Literal["comments"]]): # Tests -type AttrNames[T] = tuple[*[f.name for f in Iter[Attrs[T]]]] +type AttrNames[T] = tuple[*Map(f.name for f in Iter[Attrs[T]])] def test_qblike_3_select_01(): diff --git a/tests/test_schemalike.py b/tests/test_schemalike.py index c982533..62a9be1 100644 --- a/tests/test_schemalike.py +++ b/tests/test_schemalike.py @@ -6,6 +6,7 @@ from typemap_extensions import ( NewProtocol, Iter, + Map, Attrs, Member, NamedParam, @@ -42,8 +43,8 @@ class Property: type Schemaify[T] = NewProtocol[ - *[p for p in Iter[Attrs[T]]], - *[ + *Map(p for p in Iter[Attrs[T]]), + *Map( Member[ Concat[Literal["get_"], p.name], Callable[ @@ -56,7 +57,7 @@ class Property: Literal["ClassVar"], ] for p in Iter[Attrs[T]] - ], + ), ] diff --git a/tests/test_ts_utility.py b/tests/test_ts_utility.py index 5eed2a5..c0bf4f6 100644 --- a/tests/test_ts_utility.py +++ b/tests/test_ts_utility.py @@ -48,66 +48,68 @@ class TodoTD(TypedDict): # Pick # Constructs a type by picking the set of properties Keys from T. type Pick[T, Keys] = typing.NewProtocol[ - *[ + *typing.Map( p for p in typing.Iter[typing.Members[T]] if typing.IsAssignable[p.name, Keys] - ] + ) ] # Omit # Constructs a type by picking all properties from T and then removing Keys. # Note that unlike in TS, our Omit does not depend on Exclude. type Omit[T, Keys] = typing.NewProtocol[ - *[ + *typing.Map( p for p in typing.Iter[typing.Members[T]] if not typing.IsAssignable[p.name, Keys] - ] + ) ] # KeyOf[T] # Constructs a union of the names of every member of T. -type KeyOf[T] = Union[*[p.name for p in typing.Iter[typing.Members[T]]]] +type KeyOf[T] = Union[ + *typing.Map(p.name for p in typing.Iter[typing.Members[T]]) +] # Exclude # Constructs a type by excluding from T all union members assignable to U. type Exclude[T, U] = Union[ - *[ + *typing.Map( x for x in typing.Iter[typing.FromUnion[T]] if not typing.IsAssignable[x, U] - ] + ) ] # Extract # Constructs a type by extracting from T all union members assignable to U. type Extract[T, U] = Union[ - *[ + *typing.Map( x for x in typing.Iter[typing.FromUnion[T]] # Just the inverse of Exclude, really if typing.IsAssignable[x, U] - ] + ) ] # Partial # Constructs a type with all properties of T set to optional (T | None). type Partial[T] = typing.NewProtocol[ - *[ + *typing.Map( typing.Member[p.name, p.type | None, p.quals] for p in typing.Iter[typing.Attrs[T]] - ] + ) ] # PartialTD # Like Partial, but for TypedDicts: wraps all fields in NotRequired # rather than making them T | None. type PartialTD[T] = typing.NewTypedDict[ - *[ + *typing.Map( typing.Member[p.name, p.type, p.quals | Literal["NotRequired"]] for p in typing.Iter[typing.Attrs[T]] - ] + ) ] # End PEP section: Utility types diff --git a/tests/test_type_dir.py b/tests/test_type_dir.py index 894c5d2..7c7135d 100644 --- a/tests/test_type_dir.py +++ b/tests/test_type_dir.py @@ -13,6 +13,7 @@ InitField, IsAssignable, Iter, + Map, Member, Members, NewProtocol, @@ -86,37 +87,37 @@ class Final(Mine, Ordinary, Wrapper[float], AnotherBase[float], Last[int]): type AllOptional[T] = NewProtocol[ - *[Member[p.name, p.type | None, p.quals] for p in Iter[Attrs[T]]] + *Map(Member[p.name, p.type | None, p.quals] for p in Iter[Attrs[T]]) ] type OptionalFinal = AllOptional[Final] type Capitalize[T] = NewProtocol[ - *[Member[Uppercase[p.name], p.type, p.quals] for p in Iter[Attrs[T]]] + *Map(Member[Uppercase[p.name], p.type, p.quals] for p in Iter[Attrs[T]]) ] type Prims[T] = NewProtocol[ - *[p for p in Iter[Attrs[T]] if IsAssignable[p.type, int | str]] + *Map(p for p in Iter[Attrs[T]] if IsAssignable[p.type, int | str]) ] type NoLiterals1[T] = NewProtocol[ - *[ + *Map( Member[ p.name, Union[ - *[ + *Map( t for t in Iter[FromUnion[p.type]] # XXX: 'typing.Literal' is not *really* a type... # Maybe we can't do this, which maybe is fine. if not IsAssignable[t, Literal] - ] + ) ], p.quals, ] for p in Iter[Attrs[T]] - ] + ) ] @@ -136,23 +137,23 @@ class Final(Mine, Ordinary, Wrapper[float], AnotherBase[float], Last[int]): ) type NoLiterals2[T] = NewProtocol[ - *[ + *Map( Member[ p.name, Union[ - *[ + *Map( t for t in Iter[FromUnion[p.type]] # XXX: 'typing.Literal' is not *really* a type... # Maybe we can't do this, which maybe is fine. # if not IsAssignabletype[t, Literal] if not IsAssignable[IsLiteral[t], Literal[True]] - ] + ) ], p.quals, ] for p in Iter[Attrs[T]] - ] + ) ] diff --git a/tests/test_type_eval.py b/tests/test_type_eval.py index 136ba6b..bfc29a4 100644 --- a/tests/test_type_eval.py +++ b/tests/test_type_eval.py @@ -42,6 +42,7 @@ Iter, Length, IsEquivalent, + Map, Member, Members, NewProtocol, @@ -72,19 +73,19 @@ class F_int(F[int]): type ConcatTuples[A, B] = tuple[ - *[x for x in Iter[A]], - *[x for x in Iter[B]], + *Map(x for x in Iter[A]), + *Map(x for x in Iter[B]), ] type MapRecursive[A] = NewProtocol[ - *[ + *Map( ( Member[p.name, OrGotcha[p.type]] if not IsAssignable[p.type, A] else Member[p.name, OrGotcha[MapRecursive[A]]] ) for p in Iter[tuple[*Attrs[A], *Attrs[F_int]]] - ], + ), Member[Literal["control"], float], ] @@ -658,13 +659,13 @@ class A: def f[T]( self: T, ) -> tuple[ - *[ + *Map( m.type for m in Iter[Attrs[T]] if not IsAssignable[ Slice[m.name, None, Literal[1]], Literal["_"] ] - ] + ) ]: ... class B(A): @@ -698,13 +699,13 @@ class A: def f[T]( cls: type[T], ) -> tuple[ - *[ + *Map( m.type for m in Iter[Attrs[T]] if not IsAssignable[ Slice[m.name, None, Literal[1]], Literal["_"] ] - ] + ) ]: ... class B(A): @@ -855,10 +856,10 @@ def test_eval_getarg_callable_02(): eval_typing(GetArg[gc, GenericCallable, Literal[1]]) -type IndirectProtocol[T] = NewProtocol[*[m for m in Iter[Members[T]]],] +type IndirectProtocol[T] = NewProtocol[*Map(m for m in Iter[Members[T]]),] type GetMethodLike[T, Name] = GetArg[ tuple[ - *[ + *Map( p.type for p in Iter[Members[T]] if ( @@ -868,7 +869,7 @@ def test_eval_getarg_callable_02(): or IsAssignable[p.type, GenericCallable] ) and IsAssignable[Name, p.name] - ], + ), ], tuple, Literal[0], @@ -1529,7 +1530,9 @@ def test_eval_iter_01(): assert tuple(d) == () -type DuplicateTuple[T] = tuple[*[x for x in Iter[T]], *[x for x in Iter[T]]] +type DuplicateTuple[T] = tuple[ + *Map(x for x in Iter[T]), *Map(x for x in Iter[T]) +] type ConcatTupleWithSelf[T] = ConcatTuples[T, T] @@ -1545,6 +1548,28 @@ def test_eval_iter_02(): assert d == tuple[int, str, int, str] +type TupleFromIter[T] = tuple[*Map(x for x in Iter[T])] +type TupleAroundIter[T] = tuple[int, *Map(x for x in Iter[T]), str] +type ProtoFromIter[T] = NewProtocol[*Map(Member[m.name, int] for m in Iter[T])] + + +def test_eval_iter_any_01(): + # tuple[*Map(... Iter[Any])] collapses to Any + assert eval_typing(TupleFromIter[Any]) is Any + assert eval_typing(TupleFromIter[tuple[int, str]]) == tuple[int, str] + + +def test_eval_iter_any_02(): + # _UnpackAny propagates out of mixed positional args + assert eval_typing(TupleAroundIter[Any]) is Any + assert eval_typing(TupleAroundIter[tuple[float]]) == tuple[int, float, str] + + +def test_eval_iter_any_03(): + # Works through NewProtocol when Map's body references m attributes + assert eval_typing(ProtoFromIter[Any]) is Any + + type NotLiteralGeneric[T] = not T type AndLiteralGeneric[L, R] = L and R type OrLiteralGeneric[L, R] = L or R @@ -1993,11 +2018,11 @@ def g(self) -> int: ... # kept type MembersExceptInitSubclass[T] = tuple[ - *[ + *Map( m for m in Iter[Members[T]] if not IsAssignable[m.name, Literal["__init_subclass__"]] - ] + ) ] @@ -2080,7 +2105,7 @@ def g(self) -> int: ... # kept type AttrsAsSets[T] = UpdateClass[ - *[Member[m.name, set[m.type]] for m in Iter[Attrs[T]]] + *Map(Member[m.name, set[m.type]] for m in Iter[Attrs[T]]) ] @@ -2511,7 +2536,7 @@ class B(A): def __init_subclass__[T]( cls: type[T], ) -> UpdateClass[ - *[Member[m.name, list[m.type]] for m in Iter[Attrs[T]]] + *Map(Member[m.name, list[m.type]] for m in Iter[Attrs[T]]) ]: super().__init_subclass__() @@ -2521,7 +2546,7 @@ class C: def __init_subclass__[T]( cls: type[T], ) -> UpdateClass[ - *[Member[m.name, tuple[m.type]] for m in Iter[Attrs[T]]] + *Map(Member[m.name, tuple[m.type]] for m in Iter[Attrs[T]]) ]: super().__init_subclass__() diff --git a/tests/test_ziplike.py b/tests/test_ziplike.py index c01e288..0109b58 100644 --- a/tests/test_ziplike.py +++ b/tests/test_ziplike.py @@ -20,7 +20,7 @@ def zip[*Ts]( *args: *Ts, strict: bool = False -) -> Iterator[tuple[*[ElemOf[t] for t in typing.Iter[tuple[*Ts]]]]]: +) -> Iterator[tuple[*typing.Map(ElemOf[t] for t in typing.Iter[tuple[*Ts]])]]: return builtins.zip(*args, strict=strict) # type: ignore[call-overload] @@ -74,10 +74,10 @@ def zip_pairs[*Ts, *Us]( # single Literal iff they agree. type First[T] = typing.GetArg[T, tuple, Literal[0]] -type DropLastEach[T] = tuple[*[DropLast[t] for t in typing.Iter[T]]] -type LastEach[T] = tuple[*[Last[t] for t in typing.Iter[T]]] +type DropLastEach[T] = tuple[*typing.Map(DropLast[t] for t in typing.Iter[T])] +type LastEach[T] = tuple[*typing.Map(Last[t] for t in typing.Iter[T])] type AllSameLength[T] = typing.IsEquivalent[ - Union[*[typing.Length[t] for t in typing.Iter[T]]], + Union[*typing.Map(typing.Length[t] for t in typing.Iter[T])], typing.Length[First[T]], ] diff --git a/typemap/type_eval/_apply_generic.py b/typemap/type_eval/_apply_generic.py index fc43df1..00c5f9c 100644 --- a/typemap/type_eval/_apply_generic.py +++ b/typemap/type_eval/_apply_generic.py @@ -424,11 +424,12 @@ def flatten_class_new_proto(cls: type) -> type: # It works except for methods, since NewProtocol doesn't understand those. from typemap.typing import ( Iter, + Map, Members, NewProtocol, ) - type ClsAlias = NewProtocol[*[m for m in Iter[Members[cls]]]] # type: ignore[valid-type] + type ClsAlias = NewProtocol[*Map(m for m in Iter[Members[cls]])] # type: ignore[valid-type, type-arg] nt = _eval_typing.eval_typing(ClsAlias) args = typing.get_args(cls) diff --git a/typemap/type_eval/_eval_operators.py b/typemap/type_eval/_eval_operators.py index cb9a799..1d9b4d0 100644 --- a/typemap/type_eval/_eval_operators.py +++ b/typemap/type_eval/_eval_operators.py @@ -434,10 +434,24 @@ def wrapper(*args, ctx): ################################################################## +def _iter_any_raiser(): + """Iterator whose first __next__ raises IterAnyError. + + Raising from __next__ (rather than from Iter's __iter__) lets Map's + __iter__ catch the exception: CPython evaluates the outermost + iterable of a generator expression eagerly at construction time, so + raising from __iter__ would escape before Map ever sees it. + """ + raise IterAnyError("Iter[Any] cannot be iterated") + yield # unreachable; makes this a generator + + @type_eval.register_evaluator(Iter) @_lift_evaluated def _eval_Iter(tp, *, ctx): tp = _eval_types(tp, ctx) + if tp is typing.Any: + return _iter_any_raiser() if ( _typing_inspect.is_generic_alias(tp) and tp.__origin__ is tuple @@ -1212,6 +1226,12 @@ class TypeMapError(TypeError): pass +class IterAnyError(TypeMapError): + """Raised when Iter[Any] is iterated; caught inside _eval_args.""" + + pass + + @type_eval.register_evaluator(RaiseError) @_lift_evaluated def _eval_RaiseError(msg, *extra_types, ctx): diff --git a/typemap/type_eval/_eval_typing.py b/typemap/type_eval/_eval_typing.py index 9830bf6..8565152 100644 --- a/typemap/type_eval/_eval_typing.py +++ b/typemap/type_eval/_eval_typing.py @@ -15,7 +15,12 @@ _UnpackGenericAlias as typing_UnpackGenericAlias, ) -from typemap.typing import _AssociatedTypeGenericAlias +from typemap.typing import ( + _AssociatedTypeGenericAlias, + _UnpackAny, + _UnpackedMap, + _UnpackedMapEnd, +) if typing.TYPE_CHECKING: @@ -333,20 +338,43 @@ def _eval_type_alias(obj: typing.TypeAliasType, ctx: EvalContext): return _eval_types(unpacked, ctx) -def _eval_args(args: Sequence[Any], ctx: EvalContext) -> tuple[Any]: - evaled = [] - for arg in args: - ev = _eval_types(arg, ctx) - if isinstance(ev, typing_UnpackGenericAlias): - if (args := ev.__typing_unpacked_tuple_args__) is not None: - evaled.extend(args) - else: - evaled.append(ev) +def _eval_and_append(evaled: list, arg: Any, ctx: EvalContext) -> None: + ev = _eval_types(arg, ctx) + if isinstance(ev, typing_UnpackGenericAlias): + if (sub := ev.__typing_unpacked_tuple_args__) is not None: + evaled.extend(sub) else: evaled.append(ev) + else: + evaled.append(ev) + + +def _eval_args(args: Sequence[Any], ctx: EvalContext) -> tuple[Any, ...]: + from typemap.type_eval._eval_operators import IterAnyError + + evaled: list[Any] = [] + for arg in args: + if arg is _UnpackedMapEnd: + continue + if isinstance(arg, _UnpackedMap): + # We need to evaluate the inner arguments as we iterate, + # rather than converting to a list first, since there + # might be inner iterators that depend on the outer + # variables. + try: + for v in arg.map._gen: + _eval_and_append(evaled, v, ctx) + except IterAnyError: + evaled.append(_UnpackAny) + continue + _eval_and_append(evaled, arg, ctx) return tuple(evaled) +def _has_unpack_any(args: typing.Iterable[Any]) -> bool: + return any(a is _UnpackAny for a in args) + + @_eval_types_impl.register def _eval_applied_type_alias(obj: types.GenericAlias, ctx: EvalContext): """Eval a types.GenericAlias -- typically an applied type alias @@ -365,6 +393,8 @@ def _eval_applied_type_alias(obj: types.GenericAlias, ctx: EvalContext): return typing.Unpack[_eval_types(stripped, ctx)] new_args = _eval_args(obj.__args__, ctx) + if _has_unpack_any(new_args): + return typing.Any new_obj = _apply_type(obj.__origin__, new_args) if isinstance(obj.__origin__, type): @@ -428,6 +458,8 @@ def _eval_applied_class(obj: typing_GenericAlias, ctx: EvalContext): # generic *classes* are typing._GenericAlias while generic type # aliases are types.GenericAlias? Why in the world. new_args = _eval_args(typing.get_args(obj), ctx) + if _has_unpack_any(new_args): + return typing.Any if func := _eval_funcs.get(obj.__origin__): _tvars = ( @@ -461,6 +493,7 @@ def _eval_ty_or_list(obj): @_eval_types_impl.register def _eval_union(obj: typing.Union, ctx: EvalContext): - args: typing.Sequence[typing.Any] = obj.__args__ - new_args = tuple(_eval_types(arg, ctx) for arg in args) + new_args = _eval_args(obj.__args__, ctx) + if _has_unpack_any(new_args): + return typing.Any return typing.Union[new_args] diff --git a/typemap/typing.py b/typemap/typing.py index b258951..6e624da 100644 --- a/typemap/typing.py +++ b/typemap/typing.py @@ -327,6 +327,55 @@ class RaiseError[S: str, *Ts]: pass +class _UnpackAnyMarker: + def __repr__(self): + return "_UnpackAny" + + +# Sentinel emitted by the evaluator when a Map's genexpr raises +# IterAnyError (e.g. for Iter[Any]). When seen as an argument to a type +# operator during evaluation, the surrounding type is collapsed to Any. +_UnpackAny = _UnpackAnyMarker() + + +class _UnpackedMap: + """Wrapper yielded by Map.__iter__ so the evaluator drives iteration.""" + + __slots__ = ('map',) + + def __init__(self, map_inst): + self.map = map_inst + + def __repr__(self): + return f"_UnpackedMap({self.map!r})" + + +class _UnpackedMapEndMarker: + __slots__ = () + + def __repr__(self): + return "_UnpackedMapEnd" + + +# Paired with _UnpackedMap in Map.__iter__'s output so that +# `Union[*Map(...)]` -- which Python would otherwise collapse +# (Union[X] == X) -- keeps at least two args and reaches _eval_union +# intact. Stripped by _eval_args. +_UnpackedMapEnd = _UnpackedMapEndMarker() + + +class Map: + def __init__(self, gen): + self._gen = gen + + def __iter__(self): + # Iteration of the underlying generator is deferred to the + # evaluator so that Iter[Any] (and any other type-level errors + # raised from the body) can be handled in one place. + yield _UnpackedMap(self) + yield _UnpackedMapEnd + + ################################################################## # TODO: type better diff --git a/uv.lock b/uv.lock index d13dd20..9995275 100644 --- a/uv.lock +++ b/uv.lock @@ -56,8 +56,8 @@ wheels = [ [[package]] name = "mypy" -version = "1.20.0+dev.fbc5d6c16834379307857318e6c32326b5d8a201" -source = { git = "https://github.com/msullivan/mypy-typemap?rev=fbc5d6c16834379307857318e6c32326b5d8a201#fbc5d6c16834379307857318e6c32326b5d8a201" } +version = "1.20.0+dev.5250279d38109fedafff709488939c38901783bd" +source = { git = "https://github.com/msullivan/mypy-typemap?rev=5250279d38109fedafff709488939c38901783bd#5250279d38109fedafff709488939c38901783bd" } dependencies = [ { name = "librt", marker = "platform_python_implementation != 'PyPy'" }, { name = "mypy-extensions" }, @@ -168,7 +168,7 @@ test = [ [package.metadata.requires-dev] test = [ - { name = "mypy", git = "https://github.com/msullivan/mypy-typemap?rev=fbc5d6c16834379307857318e6c32326b5d8a201" }, + { name = "mypy", git = "https://github.com/msullivan/mypy-typemap?rev=5250279d38109fedafff709488939c38901783bd" }, { name = "pytest", specifier = ">=7.0" }, { name = "ruff" }, ]