From bd771c39b8313098961173cbd2d77058268edb70 Mon Sep 17 00:00:00 2001 From: "Michael J. Sullivan" Date: Fri, 13 Feb 2026 14:01:37 -0800 Subject: [PATCH 1/2] Update the PEP draft to reflect commiting to dot member notation --- pep.rst | 149 ++++++++++++++++-------------- typemap/type_eval/_eval_typing.py | 6 +- typemap/typing.py | 32 ++++--- 3 files changed, 100 insertions(+), 87 deletions(-) diff --git a/pep.rst b/pep.rst index af66c58..cbadcca 100644 --- a/pep.rst +++ b/pep.rst @@ -496,6 +496,9 @@ imported qualified or with some other name) # *[... for t in ...] arguments | [ +] + # Type member access (associated type access?) + | . + | GenericCallable[, lambda : ] # Type conditional checks are boolean compositions of @@ -524,8 +527,8 @@ imported qualified or with some other name) (```` is identical to ```` except that the result type is a ```` instead of a ````.) -There are three core syntactic features introduced: type booleans, -conditional types and unpacked comprehension types. +There are three and a half core syntactic features introduced: type booleans, +conditional types, unpacked comprehension types, and type member access. :ref:`"Generic callables" ` are also technically a syntactic feature, but are discussed as an operator. @@ -572,6 +575,18 @@ comprehension iterating over the arguments of tuple type ``iter_ty``. The comprehension may also have ``if`` clauses, which filter in the usual way. +Type member access +'''''''''''''''''' + +The ``Member`` and ``Param`` types introduced to represent class +members and function params have "associated" type members, which can +be accessed by dot notation: ``m.name``, ``m.type``, etc. + +This operation is not lifted over union types. Using it on the wrong +sort of type will be an error. (At least, it must be that way at +runtime, and we probably want typechecking to match.) + + Type operators -------------- @@ -1325,75 +1340,6 @@ AKA '"Rejected" Ideas That Maybe We Should Actually Do?' Very interested in feedback about these! -The first one in particular I think has a lot of upside. - -Support dot notation to access ``Member`` components ----------------------------------------------------- - -Code would read quite a bit nicer if we could write ``m.name`` instead -of ``GetName[m]``. -With dot notation, ``PropsOnly`` (from -:ref:`the query builder example `) would look like:: - - type PropsOnly[T] = typing.NewProtocol[ - *[ - typing.Member[p.name, PointerArg[p.type]] - for p in typing.Iter[typing.Attrs[T]] - if typing.IsAssignable[p.type, Property] - ] - ] - -Which is a fair bit nicer. - - -We considered this but initially rejected it in part due to runtime -implementation concerns: an expression like ``Member[Literal["x"], -int].name`` would need to return an object that captures both the -content of the type alias while maintaining the ``_GenericAlias`` of -the applied class so that type variables may be substituted for. - -We were mistaken about the runtime evaluation difficulty, -though: if we required a special base class in order for a type to use -this feature, it should work without too much trouble, and without -causing any backporting or compatibility problems. - -We wouldn't be able to have the operation lift over unions or the like -(unless we were willing to modify ``__getattr__`` for -``types.UnionType`` and ``typing._UnionGenericAlias`` to do so!) - -Or maybe it would be fine to have it only work on variables, and then -no special support would be required at the definition site. - -That just leaves semantic and philosophical concerns: it arguably makes -the model more complicated, but a lot of code will read much nicer. - -What would the mechanism be? -'''''''''''''''''''''''''''' - -A general mechanism to support this might look -like:: - - class Member[ - N: str, - T, - Q: MemberQuals = typing.Never, - I = typing.Never, - D = typing.Never - ]: - type name = N - type tp = T - type quals = Q - type init = I - type definer = D - -Where ``type`` aliases defined in a class can be accessed by dot notation. - - -Another option would be to skip introducing a general mechanism (for -now, at least), but at least make dot notation work on ``Member`` and -``Param``, which will be extremely common. - - Dictionary comprehension based syntax for creating typed dicts and protocols ---------------------------------------------------------------------------- @@ -1486,6 +1432,38 @@ difference? Combined with dictionary-comprehensions and dot notation (The user-defined type alias ``PointerArg`` still must be called with brackets, despite being basically a helper operator.) +Have a general mechanism for dot-notation accessible associated types +--------------------------------------------------------------------- + +The main proposal is currently silent about exactly *how* ``Member`` +and ``Param`` will have associated types for ``.name`` and ``.type``. + +We could just make it work for those particular types, or we could +introduce a general mechansim that might look something like:: + + @typing.has_associated_types + class Member[ + N: str, + T, + Q: MemberQuals = typing.Never, + I = typing.Never, + D = typing.Never + ]: + type name = N + type tp = T + type quals = Q + type init = I + type definer = D + + +The decorator (or a base class) is needed if we want the dot notation +for the associated types to be able to work at runtime, since we need +to customize the behavior of ``__getattr__`` on the +``typing._GenericAlias`` produced by the class so that it captures +both the type parameters to ``Member`` and the alias. + +(Though possibly we could change the behavior of ``_GenericAlias`` +itself to avoid the need for that.) Rejected Ideas ============== @@ -1567,6 +1545,35 @@ worse. Supporting filtering while mapping would make it even more bad We can explore other options too if needed. + +Don't use dot notation to access ``Member`` components +------------------------------------------------------ + +Earlier versions of this PEP draft omitted the ability to write +``m.name`` and similar on ``Member`` and ``Param`` components, and +instead relied on helper operators such as ``typing.GetName`` (that +could be implemented under the hood using ``typing.GetArg`` or +``typing.GetMemberType``). + +The potential advantage here is reducing the number of new constructs +being added to the type language, and avoiding needing to either +introduce a new general mechanism for associated types or having a +special-case for ``Member``. + +``PropsOnly`` (from :ref:`the query builder example `) would +look like:: + + type PropsOnly[T] = typing.NewProtocol[ + *[ + typing.Member[typing.GetName[p], PointerArg[typing.GetType[p]]] + for p in typing.Iter[typing.Attrs[T]] + if typing.IsAssignable[typing.GetType[p], Property] + ] + ] + +Everyone hated how this looked a lot. + + Perform type manipulations with normal Python functions ------------------------------------------------------- diff --git a/typemap/type_eval/_eval_typing.py b/typemap/type_eval/_eval_typing.py index 2fb5c9c..aa0a1ee 100644 --- a/typemap/type_eval/_eval_typing.py +++ b/typemap/type_eval/_eval_typing.py @@ -15,7 +15,7 @@ _UnpackGenericAlias as typing_UnpackGenericAlias, ) -from typemap.typing import _NestedGenericAlias +from typemap.typing import _AssociatedTypeGenericAlias if typing.TYPE_CHECKING: @@ -400,7 +400,9 @@ def _eval_applied_type_alias(obj: types.GenericAlias, ctx: EvalContext): @_eval_types_impl.register -def _eval_nested_generic_alias(obj: _NestedGenericAlias, ctx: EvalContext): +def _eval_nested_generic_alias( + obj: _AssociatedTypeGenericAlias, ctx: EvalContext +): base, alias = obj.__args__ # TODO: what if it has parameters of its own diff --git a/typemap/typing.py b/typemap/typing.py index ad3377c..9012087 100644 --- a/typemap/typing.py +++ b/typemap/typing.py @@ -57,23 +57,29 @@ def __typing_unpacked_tuple_args__(self): ### -class SupportAliases: - @classmethod +def has_associated_types(ocls): def __class_getitem__(cls, args): - # Return an _AliasSupportingGenericAlias instead of a _GenericAlias - res = super().__class_getitem__(args) - return _AliasSupportingGenericAlias(res.__origin__, res.__args__) + # Return an _HasAssociatedTypesGenericAlias instead of a _GenericAlias + res = super(ocls, cls).__class_getitem__(args) + return _HasAssociatedTypesGenericAlias(res.__origin__, res.__args__) + + ocls.__class_getitem__ = classmethod(__class_getitem__) + return ocls -class _NestedGenericAlias(_GenericAlias, _root=True): +class _AssociatedTypeGenericAlias(_GenericAlias, _root=True): pass -class _AliasSupportingGenericAlias(_GenericAlias, _root=True): +class _AssociatedType[Obj, Alias]: + pass + + +class _HasAssociatedTypesGenericAlias(_GenericAlias, _root=True): def __getattr__(self, attr): res = super().__getattr__(attr) if isinstance(res, typing.TypeAliasType): - res = _NestedGenericAlias(NestedAlias, (self, res)) + res = _AssociatedTypeGenericAlias(_AssociatedType, (self, res)) return res @@ -177,13 +183,14 @@ class DropAnnotations[T]: MemberQuals = Literal["ClassVar", "Final", "NotRequired", "ReadOnly"] +@has_associated_types class Member[ N: str, T, Q: MemberQuals = typing.Never, I = typing.Never, D = typing.Never, -](SupportAliases): +]: type name = N type type = T type quals = Q @@ -194,7 +201,8 @@ class Member[ ParamQuals = Literal["*", "**", "keyword", "positional", "default"] -class Param[N: str | None, T, Q: ParamQuals = typing.Never](SupportAliases): +@has_associated_types +class Param[N: str | None, T, Q: ParamQuals = typing.Never]: type name = N type type = T type quals = Q @@ -218,10 +226,6 @@ class Param[N: str | None, T, Q: ParamQuals = typing.Never](SupportAliases): type GetDefiner[T: Member] = T.definer -class NestedAlias[Obj, Alias]: - pass - - class Attrs[T](_TupleLikeOperator): pass From f0e9f35f5f74901e115a223765f32b05a5129dff Mon Sep 17 00:00:00 2001 From: "Michael J. Sullivan" Date: Fri, 13 Feb 2026 17:18:50 -0800 Subject: [PATCH 2/2] Update all of the tests to use dot notation for Member This also requires fixing a bunch in the evaluator to support dealing with yet another way that an expression might get Stuck (by AttributeError on a TypeVar). --- pep.rst | 56 ++++++--------- tests/test_astlike_1.py | 6 +- tests/test_call.py | 3 +- tests/test_eval_call_with_types.py | 10 ++- tests/test_fastapilike_1.py | 31 +++------ tests/test_qblike.py | 8 +-- tests/test_qblike_2.py | 10 +-- tests/test_qblike_3.py | 52 ++++++-------- tests/test_schemalike.py | 6 +- tests/test_type_dir.py | 45 +++++++----- tests/test_type_eval.py | 104 +++++++++++++--------------- typemap/type_eval/_apply_generic.py | 32 +++++++-- typemap/type_eval/_eval_call.py | 8 ++- typemap/typing.py | 2 +- 14 files changed, 184 insertions(+), 189 deletions(-) diff --git a/pep.rst b/pep.rst index cbadcca..9e7201e 100644 --- a/pep.rst +++ b/pep.rst @@ -689,13 +689,14 @@ Object inspection of classes. Its type parameters encode the information about each member. - * ``N`` is the name, as a literal string type - * ``T`` is the type - * ``Q`` is a union of qualifiers (see ``MemberQuals`` below) + * ``N`` is the name, as a literal string type. Accessable with ``.name``. + * ``T`` is the type. Accessable with ``.type``. + * ``Q`` is a union of qualifiers (see ``MemberQuals`` below). Accessable with ``.quals``. * ``Init`` is the literal type of the attribute initializer in the - class (see :ref:`InitField `) + class (see :ref:`InitField `). Accessable with ``.init``. * ``D`` is the defining class of the member. (That is, which class - the member is inherited from. Always ``Never``, for a ``TypedDict``) + the member is inherited from. Always ``Never``, for a ``TypedDict``). + Accessable with ``.definer``. * ``MemberQuals = Literal['ClassVar', 'Final', 'NotRequired', 'ReadOnly']`` - ``MemberQuals`` is the type of "qualifiers" that can apply to a @@ -709,16 +710,6 @@ qualifier. ``staticmethod`` and ``classmethod`` will return ``staticmethod`` and ``classmethod`` types, which are subscriptable as of 3.14. -We also have helpers for extracting the fields of ``Members``; they -are all definable in terms of ``GetArg``. (Some of them are shared -with ``Param``, discussed below.) - -* ``GetName[T: Member | Param]`` -* ``GetType[T: Member | Param]`` -* ``GetQuals[T: Member | Param]`` -* ``GetInit[T: Member]`` -* ``GetDefiner[T: Member]`` - All of the operators in this section are :ref:`lifted over union types `. @@ -810,10 +801,10 @@ Callable inspection and creation ``Callable`` types always have their arguments exposed in the extended Callable format discussed above. -The names, type, and qualifiers share getter operations with -``Member``. +The names, type, and qualifiers share associated type names with +``Member`` (``.name``, ``.type``, and ``.quals``). -TODO: Should we make ``GetInit`` be literal types of default parameter +TODO: Should we make ``.init`` be literal types of default parameter values too? .. _generic-callable: @@ -962,8 +953,7 @@ those cases, we add a new hook to ``typing``: it before being returned. If set to ``None`` (the default), the boolean operators will return - ``False`` while ``Iter`` will evaluate to - ``iter(typing.TypeVarTuple("_IterDummy"))``. + ``False`` while ``Iter`` will evaluate to ``iter(())``. There has been some discussion of adding a ``Format.AST`` mode for @@ -1012,7 +1002,7 @@ The ``**kwargs: Unpack[K]`` is part of this proposal, and allows type-annotated attribute of ``K``, while calling ``NewProtocol`` with ``Member`` arguments constructs a new structural type. -``GetName`` is a getter operator that fetches the name of a ``Member`` +``c.name`` fetches the name of the ``Member`` bound to the variable ``c`` as a literal type--all of these mechanisms lean very heavily on literal types. ``GetMemberType`` gets the type of an attribute from a class. @@ -1026,8 +1016,8 @@ as a literal type--all of these mechanisms lean very heavily on literal types. typing.NewProtocol[ *[ typing.Member[ - typing.GetName[c], - ConvertField[typing.GetMemberType[ModelT, typing.GetName[c]]], + c.name, + ConvertField[typing.GetMemberType[ModelT, c.name]], ] for c in typing.Iter[typing.Attrs[K]] ] @@ -1081,9 +1071,9 @@ contains all the ``Property`` attributes of ``T``. type PropsOnly[T] = typing.NewProtocol[ *[ - typing.Member[typing.GetName[p], PointerArg[typing.GetType[p]]] + typing.Member[p.name, PointerArg[p.type]] for p in typing.Iter[typing.Attrs[T]] - if typing.IsAssignable[typing.GetType[p], Property] + if typing.IsAssignable[p.type, Property] ] ] @@ -1113,15 +1103,15 @@ suite, but here is a possible implementation of just ``Public`` type Create[T] = typing.NewProtocol[ *[ typing.Member[ - typing.GetName[p], - typing.GetType[p], - typing.GetQuals[p], - GetDefault[typing.GetInit[p]], + p.name, + p.type, + p.quals, + GetDefault[p.init], ] for p in typing.Iter[typing.Attrs[T]] if not typing.IsAssignable[ Literal[True], - GetFieldItem[typing.GetInit[p], Literal["primary_key"]], + GetFieldItem[p.init, Literal["primary_key"]], ] ] ] @@ -1153,13 +1143,13 @@ dataclasses-style method generation typing.Param[Literal["self"], Self], *[ typing.Param[ - typing.GetName[p], - typing.GetType[p], + p.name, + p.type, # All arguments are keyword-only # It takes a default if a default is specified in the class Literal["keyword"] if typing.IsAssignable[ - GetDefault[typing.GetInit[p]], + GetDefault[p.init], Never, ] else Literal["keyword", "default"], diff --git a/tests/test_astlike_1.py b/tests/test_astlike_1.py index 78fe89b..a9bdf42 100644 --- a/tests/test_astlike_1.py +++ b/tests/test_astlike_1.py @@ -9,8 +9,6 @@ BaseTypedDict, Bool, GetArg, - GetName, - GetType, IsAssignable, Iter, IsEquivalent, @@ -139,8 +137,8 @@ def test_astlike_1_combine_varargs_02(): or Bool[IsEquivalent[L, complex] and Bool[IsComplex[R]]] ) type VarIsPresent[V: VarArg, K: BaseTypedDict] = any( - IsEquivalent[VarArgName[V], GetName[x]] - and Bool[IsNumericAssignable[VarArgType[V], GetType[x]]] + IsEquivalent[VarArgName[V], x.name] + and Bool[IsNumericAssignable[VarArgType[V], x.type]] for x in Iter[Attrs[K]] ) type AllVarsPresent[Vs: tuple[VarArg, ...], K: BaseTypedDict] = all( diff --git a/tests/test_call.py b/tests/test_call.py index 3bbccc8..8add338 100644 --- a/tests/test_call.py +++ b/tests/test_call.py @@ -8,7 +8,6 @@ BaseTypedDict, NewProtocol, Member, - GetName, Iter, ) @@ -18,7 +17,7 @@ def func[*T, K: BaseTypedDict]( *args: Unpack[T], **kwargs: Unpack[K], -) -> NewProtocol[*[Member[GetName[c], int] for c in Iter[Attrs[K]]]]: ... +) -> NewProtocol[*[Member[c.name, int] for c in Iter[Attrs[K]]]]: ... def test_call_1(): diff --git a/tests/test_eval_call_with_types.py b/tests/test_eval_call_with_types.py index 4ab0d75..e2e3e4e 100644 --- a/tests/test_eval_call_with_types.py +++ b/tests/test_eval_call_with_types.py @@ -6,8 +6,6 @@ from typemap_extensions import ( GenericCallable, GetArg, - GetName, - GetType, IsAssignable, Iter, Members, @@ -263,13 +261,13 @@ def func[T](x: C[T]) -> T: ... type GetCallableMember[T, N: str] = GetArg[ tuple[ *[ - GetType[m] + m.type for m in Iter[Members[T]] if ( - IsAssignable[GetType[m], Callable] - or IsAssignable[GetType[m], GenericCallable] + IsAssignable[m.type, Callable] + or IsAssignable[m.type, GenericCallable] ) - and IsAssignable[GetName[m], N] + and IsAssignable[m.name, N] ] ], tuple, diff --git a/tests/test_fastapilike_1.py b/tests/test_fastapilike_1.py index ca72fd6..7a0d93e 100644 --- a/tests/test_fastapilike_1.py +++ b/tests/test_fastapilike_1.py @@ -16,9 +16,6 @@ GetAnnotations, DropAnnotations, FromUnion, - GetType, - GetName, - GetQuals, Member, Members, Param, @@ -54,12 +51,12 @@ class _Default: Param[Literal["self"], Self], *[ Param[ - GetName[p], - DropAnnotations[GetType[p]], + p.name, + DropAnnotations[p.type], Literal["keyword", "default"] if IsAssignable[ Literal[PropQuals.HAS_DEFAULT], - GetAnnotations[GetType[p]], + GetAnnotations[p.type], ] else Literal["keyword"], ] @@ -95,22 +92,18 @@ class _Default: # from the DB, so we don't need default values. type Public[T] = NewProtocol[ *[ - Member[GetName[p], FixPublicType[GetType[p]], GetQuals[p]] + Member[p.name, FixPublicType[p.type], p.quals] for p in Iter[Attrs[T]] - if not IsAssignable[ - Literal[PropQuals.HIDDEN], GetAnnotations[GetType[p]] - ] + if not IsAssignable[Literal[PropQuals.HIDDEN], GetAnnotations[p.type]] ] ] # Create takes everything but the primary key and preserves defaults type Create[T] = NewProtocol[ *[ - Member[GetName[p], GetType[p], GetQuals[p]] + Member[p.name, p.type, p.quals] for p in Iter[Attrs[T]] - if not IsAssignable[ - Literal[PropQuals.PRIMARY], GetAnnotations[GetType[p]] - ] + if not IsAssignable[Literal[PropQuals.PRIMARY], GetAnnotations[p.type]] ] ] @@ -120,14 +113,12 @@ class _Default: type Update[T] = NewProtocol[ *[ Member[ - GetName[p], - HasDefault[DropAnnotations[GetType[p]] | None, None], - GetQuals[p], + p.name, + HasDefault[DropAnnotations[p.type] | None, None], + p.quals, ] for p in Iter[Attrs[T]] - if not IsAssignable[ - Literal[PropQuals.PRIMARY], GetAnnotations[GetType[p]] - ] + if not IsAssignable[Literal[PropQuals.PRIMARY], GetAnnotations[p.type]] ] ] diff --git a/tests/test_qblike.py b/tests/test_qblike.py index d1993cb..85cbd39 100644 --- a/tests/test_qblike.py +++ b/tests/test_qblike.py @@ -13,9 +13,7 @@ Iter, Attrs, IsAssignable, - GetType, Member, - GetName, GetMemberType, GetArg, ) @@ -32,7 +30,7 @@ class Link[T]: type PropsOnly[T] = NewProtocol[ - *[p for p in Iter[Attrs[T]] if IsAssignable[GetType[p], Property]] + *[p for p in Iter[Attrs[T]] if IsAssignable[p.type, Property]] ] # Conditional type alias! @@ -48,8 +46,8 @@ def select[K: BaseTypedDict]( ) -> NewProtocol[ *[ Member[ - GetName[c], - FilterLinks[GetMemberType[A, GetName[c]]], + c.name, + FilterLinks[GetMemberType[A, c.name]], ] for c in Iter[Attrs[K]] ] diff --git a/tests/test_qblike_2.py b/tests/test_qblike_2.py index 9cd0d0b..0b245a4 100644 --- a/tests/test_qblike_2.py +++ b/tests/test_qblike_2.py @@ -45,7 +45,7 @@ class MultiLink[T](Link[T]): type-annotated attribute of ``K``, while calling ``NewProtocol`` with ``Member`` arguments constructs a new structural type. -``GetName`` is a getter operator that fetches the name of a ``Member`` +``c.name`` fetches the name of the ``Member`` bound to the variable ``c`` as a literal type--all of these mechanisms lean very heavily on literal types. ``GetMemberType`` gets the type of an attribute from a class. @@ -60,8 +60,8 @@ def select[ModelT, K: typing.BaseTypedDict]( typing.NewProtocol[ *[ typing.Member[ - typing.GetName[c], - ConvertField[typing.GetMemberType[ModelT, typing.GetName[c]]], + c.name, + ConvertField[typing.GetMemberType[ModelT, c.name]], ] for c in typing.Iter[typing.Attrs[K]] ] @@ -113,9 +113,9 @@ def select[ModelT, K: typing.BaseTypedDict]( """ type PropsOnly[T] = typing.NewProtocol[ *[ - typing.Member[typing.GetName[p], PointerArg[typing.GetType[p]]] + typing.Member[p.name, PointerArg[p.type]] for p in typing.Iter[typing.Attrs[T]] - if typing.IsAssignable[typing.GetType[p], Property] + if typing.IsAssignable[p.type, Property] ] ] diff --git a/tests/test_qblike_3.py b/tests/test_qblike_3.py index bf2fd59..796c1e5 100644 --- a/tests/test_qblike_3.py +++ b/tests/test_qblike_3.py @@ -18,12 +18,8 @@ Bool, Length, GetArg, - GetInit, GetMemberType, - GetName, - GetQuals, GetSpecialAttr, - GetType, InitField, IsAssignable, Iter, @@ -142,19 +138,19 @@ def __init_subclass__[T]( ) -> UpdateClass[ *[ Member[ - GetName[m], + m.name, _Field[ - GetArg[GetType[m], Field, Literal[0]], + GetArg[m.type, Field, Literal[0]], T, - GetName[m], + m.name, ], - GetQuals[m], - GetInit[m], + m.quals, + m.init, ] for m in Iter[Members[T]] - if IsAssignable[GetType[m], Field] + if IsAssignable[m.type, Field] ], - *[m for m in Iter[Members[T]] if not IsAssignable[GetType[m], Field]], + *[m for m in Iter[Members[T]] if not IsAssignable[m.type, Field]], ]: super().__init_subclass__() @@ -196,13 +192,11 @@ class column[Args: ColumnArgs](InitField[Args]): ] type ReadValueNeverNull[M] = ( - not Bool[ColumnInitIsNullable[GetInit[M]]] - or Bool[ColumnInitIsAutoincrement[GetInit[M]]] + not Bool[ColumnInitIsNullable[M.init]] + or Bool[ColumnInitIsAutoincrement[M.init]] or ( - IsAssignable[FieldPyType[GetType[M]], list] - and IsAssignable[ - GetArg[FieldPyType[GetType[M]], list, Literal[0]], Table - ] + IsAssignable[FieldPyType[M.type], list] + and IsAssignable[GetArg[FieldPyType[M.type], list, Literal[0]], Table] ) ) @@ -247,7 +241,7 @@ class DbLinkSource[Args: DbLinkSourceArgs](InitField[Args]): *[ m for m in Iter[Attrs[T]] - if any(IsAssignable[GetName[m], f] for f in Iter[FieldNames]) + if any(IsAssignable[m.name, f] for f in Iter[FieldNames]) ] ] @@ -258,13 +252,7 @@ class DbLinkSource[Args: DbLinkSourceArgs](InitField[Args]): type MakeQueryEntryAllFields[T: Table] = QueryEntry[ T, - tuple[ - *[ - GetName[m] - for m in Iter[Attrs[T]] - if IsAssignable[GetType[m], _Field] - ], - ], + tuple[*[m.name for m in Iter[Attrs[T]] if IsAssignable[m.type, _Field]],], ] type MakeQueryEntryNamedFields[ T: Table, @@ -273,11 +261,11 @@ class DbLinkSource[Args: DbLinkSourceArgs](InitField[Args]): T, tuple[ *[ - GetName[m] + m.name for m in Iter[Attrs[T]] - if IsAssignable[GetType[m], _Field] + if IsAssignable[m.type, _Field] and any( - IsAssignable[FieldName[GetType[m]], f] for f in Iter[FieldNames] + IsAssignable[FieldName[m.type], f] for f in Iter[FieldNames] ) ], ], @@ -337,11 +325,11 @@ class Query[Es: tuple[QueryEntry[Table, tuple[Member]], ...]]: type Select[T: Table, FieldNames: tuple[Literal[str], ...]] = NewProtocol[ *[ Member[ - GetName[m], + m.name, ( - FieldPyType[GetType[m]] + FieldPyType[m.type] if Bool[ReadValueNeverNull[m]] - else FieldPyType[GetType[m]] | None + else FieldPyType[m.type] | None ), ] for m in Iter[EntryFieldMembers[T, FieldNames]] @@ -422,7 +410,7 @@ class Comment(Table[Literal["comments"]]): # Tests -type AttrNames[T] = tuple[*[GetName[f] for f in Iter[Attrs[T]]]] +type AttrNames[T] = tuple[*[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 5a80a47..1f09d25 100644 --- a/tests/test_schemalike.py +++ b/tests/test_schemalike.py @@ -7,8 +7,6 @@ NewProtocol, Iter, Attrs, - GetType, - GetName, Member, NamedParam, Param, @@ -46,13 +44,13 @@ class Property: *[p for p in Iter[Attrs[T]]], *[ Member[ - StrConcat[Literal["get_"], GetName[p]], + StrConcat[Literal["get_"], p.name], Callable[ [ Param[Literal["self"], Schemaify[T]], NamedParam[Literal["schema"], Schema, Literal["keyword"]], ], - GetType[p], + p.type, ], Literal["ClassVar"], ] diff --git a/tests/test_type_dir.py b/tests/test_type_dir.py index 19bf2d1..d35e3f3 100644 --- a/tests/test_type_dir.py +++ b/tests/test_type_dir.py @@ -7,9 +7,6 @@ Attrs, FromUnion, GetArg, - GetName, - GetQuals, - GetType, InitField, IsAssignable, Iter, @@ -86,40 +83,34 @@ class Final(Mine, Ordinary, Wrapper[float], AnotherBase[float], Last[int]): type AllOptional[T] = NewProtocol[ - *[ - Member[GetName[p], GetType[p] | None, GetQuals[p]] - for p in Iter[Attrs[T]] - ] + *[Member[p.name, p.type | None, p.quals] for p in Iter[Attrs[T]]] ] type OptionalFinal = AllOptional[Final] type Capitalize[T] = NewProtocol[ - *[ - Member[Uppercase[GetName[p]], GetType[p], GetQuals[p]] - for p in Iter[Attrs[T]] - ] + *[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[GetType[p], int | str]] + *[p for p in Iter[Attrs[T]] if IsAssignable[p.type, int | str]] ] type NoLiterals1[T] = NewProtocol[ *[ Member[ - GetName[p], + p.name, Union[ *[ t - for t in Iter[FromUnion[GetType[p]]] + 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] ] ], - GetQuals[p], + p.quals, ] for p in Iter[Attrs[T]] ] @@ -144,18 +135,18 @@ class Final(Mine, Ordinary, Wrapper[float], AnotherBase[float], Last[int]): type NoLiterals2[T] = NewProtocol[ *[ Member[ - GetName[p], + p.name, Union[ *[ t - for t in Iter[FromUnion[GetType[p]]] + 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]] ] ], - GetQuals[p], + p.quals, ] for p in Iter[Attrs[T]] ] @@ -460,3 +451,21 @@ class Inited: foo: int = 10 bar: bool = Field(foo=False) """) + + +class AnnoyingProjection: + def foo[T: typing.Member](self, a: T) -> T.name: + pass + + def bar[T: typing.Member](self, a: T) -> 'T.name': + pass + + +def test_type_dir_dot_projection_1(): + d = eval_typing(AnnoyingProjection) + + assert format_helper.format_class(d) == textwrap.dedent("""\ + class AnnoyingProjection: + foo: typing.ClassVar[typemap.typing.GenericCallable[tuple[T], <...>]] + bar: typing.ClassVar[typemap.typing.GenericCallable[tuple[T], <...>]] + """) diff --git a/tests/test_type_eval.py b/tests/test_type_eval.py index 945b6c8..bdb910e 100644 --- a/tests/test_type_eval.py +++ b/tests/test_type_eval.py @@ -31,13 +31,9 @@ GenericCallable, GetArg, GetArgs, - GetDefiner, GetMember, GetMemberType, - GetName, - GetQuals, GetSpecialAttr, - GetType, GetAnnotations, IsAssignable, Iter, @@ -79,9 +75,9 @@ class F_int(F[int]): type MapRecursive[A] = NewProtocol[ *[ ( - Member[GetName[p], OrGotcha[GetType[p]]] - if not IsAssignable[GetType[p], A] - else Member[GetName[p], OrGotcha[MapRecursive[A]]] + 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]]] ], @@ -427,11 +423,11 @@ class C: def f[TX](self, x: TX) -> OnlyIntToSet[TX]: ... m = eval_typing(GetMember[C, Literal["f"]]) - assert eval_typing(GetName[m]) == Literal["f"] - assert eval_typing(GetQuals[m]) == Literal["ClassVar"] - assert eval_typing(GetDefiner[m]) == C + assert eval_typing(m.name) == Literal["f"] + assert eval_typing(m.quals) == Literal["ClassVar"] + assert eval_typing(m.definer) == C - t = eval_typing(GetType[m]) + t = eval_typing(m.type) Vs = get_args(get_args(t)[0]) L = get_args(t)[1] f = L(*Vs) @@ -451,11 +447,11 @@ def f[T](self, x: T) -> OnlyIntToSet[T]: ... type P = IndirectProtocol[C] m = eval_typing(GetMember[P, Literal["f"]]) - assert eval_typing(GetName[m]) == Literal["f"] - assert eval_typing(GetQuals[m]) == Literal["ClassVar"] - assert eval_typing(GetDefiner[m]) != C # eval typing generates a new class + assert eval_typing(m.name) == Literal["f"] + assert eval_typing(m.quals) == Literal["ClassVar"] + assert eval_typing(m.definer) != C # eval typing generates a new class - t = eval_typing(GetType[m]) + t = eval_typing(m.type) Vs = get_args(get_args(t)[0]) L = get_args(t)[1] f = L(*Vs) @@ -479,7 +475,7 @@ def f[T](self, x: T) -> T: ... def f(self, x): ... m = eval_typing(GetMember[C, Literal["f"]]) - mt = eval_typing(GetType[m]) + mt = eval_typing(m.type) assert mt.__origin__ is Overloaded assert len(mt.__args__) == 2 @@ -520,10 +516,10 @@ class C(A): x: str m = eval_typing(GetMember[A, Literal["member_method"]]) - assert eval_typing(GetName[m]) == Literal["member_method"] - assert eval_typing(IsAssignable[GetType[m], GenericCallable]) - assert eval_typing(GetQuals[m]) == Literal["ClassVar"] - assert eval_typing(GetDefiner[m]) == A + assert eval_typing(m.name) == Literal["member_method"] + assert eval_typing(IsAssignable[m.type, GenericCallable]) + assert eval_typing(m.quals) == Literal["ClassVar"] + assert eval_typing(m.definer) == A ft = m.__args__[1].__args__[1] with _ensure_context(): @@ -548,10 +544,10 @@ class C(A): x: str m = eval_typing(GetMember[A, Literal["member_method"]]) - assert eval_typing(GetName[m]) == Literal["member_method"] - assert eval_typing(IsAssignable[GetType[m], GenericCallable]) - assert eval_typing(GetQuals[m]) == Literal["ClassVar"] - assert eval_typing(GetDefiner[m]) == A + assert eval_typing(m.name) == Literal["member_method"] + assert eval_typing(IsAssignable[m.type, GenericCallable]) + assert eval_typing(m.quals) == Literal["ClassVar"] + assert eval_typing(m.definer) == A ft = m.__args__[1].__args__[1] with _ensure_context(): @@ -588,10 +584,10 @@ class C(A): x: str m = eval_typing(GetMember[A, Literal["class_method"]]) - assert eval_typing(GetName[m]) == Literal["class_method"] - assert eval_typing(IsAssignable[GetType[m], GenericCallable]) - assert eval_typing(GetQuals[m]) == Literal["ClassVar"] - assert eval_typing(GetDefiner[m]) == A + assert eval_typing(m.name) == Literal["class_method"] + assert eval_typing(IsAssignable[m.type, GenericCallable]) + assert eval_typing(m.quals) == Literal["ClassVar"] + assert eval_typing(m.definer) == A ft = m.__args__[1].__args__[1] with _ensure_context(): @@ -615,10 +611,10 @@ class C(A): x: str m = eval_typing(GetMember[A, Literal["class_method"]]) - assert eval_typing(GetName[m]) == Literal["class_method"] - assert eval_typing(IsAssignable[GetType[m], GenericCallable]) - assert eval_typing(GetQuals[m]) == Literal["ClassVar"] - assert eval_typing(GetDefiner[m]) == A + assert eval_typing(m.name) == Literal["class_method"] + assert eval_typing(IsAssignable[m.type, GenericCallable]) + assert eval_typing(m.quals) == Literal["ClassVar"] + assert eval_typing(m.definer) == A ft = m.__args__[1].__args__[1] with _ensure_context(): @@ -643,10 +639,10 @@ def f[T]( self: T, ) -> tuple[ *[ - GetType[m] + m.type for m in Iter[Attrs[T]] if not IsAssignable[ - Slice[GetName[m], None, Literal[1]], Literal["_"] + Slice[m.name, None, Literal[1]], Literal["_"] ] ] ]: ... @@ -658,10 +654,10 @@ class B(A): _d: float m = eval_typing(GetMember[A, Literal["f"]]) - assert eval_typing(GetName[m]) == Literal["f"] - assert eval_typing(IsAssignable[GetType[m], GenericCallable]) - assert eval_typing(GetQuals[m]) == Literal["ClassVar"] - assert eval_typing(GetDefiner[m]) == A + assert eval_typing(m.name) == Literal["f"] + assert eval_typing(IsAssignable[m.type, GenericCallable]) + assert eval_typing(m.quals) == Literal["ClassVar"] + assert eval_typing(m.definer) == A ft = m.__args__[1].__args__[1] with _ensure_context(): @@ -683,10 +679,10 @@ def f[T]( cls: type[T], ) -> tuple[ *[ - GetType[m] + m.type for m in Iter[Attrs[T]] if not IsAssignable[ - Slice[GetName[m], None, Literal[1]], Literal["_"] + Slice[m.name, None, Literal[1]], Literal["_"] ] ] ]: ... @@ -698,10 +694,10 @@ class B(A): _y: float m = eval_typing(GetMember[B, Literal["f"]]) - assert eval_typing(GetName[m]) == Literal["f"] - assert eval_typing(IsAssignable[GetType[m], GenericCallable]) - assert eval_typing(GetQuals[m]) == Literal["ClassVar"] - assert eval_typing(GetDefiner[m]) == A + assert eval_typing(m.name) == Literal["f"] + assert eval_typing(IsAssignable[m.type, GenericCallable]) + assert eval_typing(m.quals) == Literal["ClassVar"] + assert eval_typing(m.definer) == A ft = m.__args__[1].__args__[1] with _ensure_context(): @@ -820,15 +816,15 @@ def test_eval_getarg_callable_02(): type GetMethodLike[T, Name] = GetArg[ tuple[ *[ - GetType[p] + p.type for p in Iter[Members[T]] if ( - IsAssignable[GetType[p], Callable] - or IsAssignable[GetType[p], staticmethod] - or IsAssignable[GetType[p], classmethod] - or IsAssignable[GetType[p], GenericCallable] + IsAssignable[p.type, Callable] + or IsAssignable[p.type, staticmethod] + or IsAssignable[p.type, classmethod] + or IsAssignable[p.type, GenericCallable] ) - and IsAssignable[Name, GetName[p]] + and IsAssignable[Name, p.name] ], ], tuple, @@ -2010,7 +2006,7 @@ def g(self) -> int: ... # omitted *[ m for m in Iter[Members[T]] - if not IsAssignable[GetName[m], Literal["__init_subclass__"]] + if not IsAssignable[m.name, Literal["__init_subclass__"]] ] ] @@ -2085,7 +2081,7 @@ def g(self) -> int: ... # omitted type AttrsAsSets[T] = UpdateClass[ - *[Member[GetName[m], set[GetType[m]]] for m in Iter[Attrs[T]]] + *[Member[m.name, set[m.type]] for m in Iter[Attrs[T]]] ] @@ -2504,7 +2500,7 @@ class B(A): def __init_subclass__[T]( cls: type[T], ) -> UpdateClass[ - *[Member[GetName[m], list[GetType[m]]] for m in Iter[Attrs[T]]] + *[Member[m.name, list[m.type]] for m in Iter[Attrs[T]]] ]: super().__init_subclass__() @@ -2514,7 +2510,7 @@ class C: def __init_subclass__[T]( cls: type[T], ) -> UpdateClass[ - *[Member[GetName[m], tuple[GetType[m]]] for m in Iter[Attrs[T]]] + *[Member[m.name, tuple[m.type]] for m in Iter[Attrs[T]]] ]: super().__init_subclass__() diff --git a/typemap/type_eval/_apply_generic.py b/typemap/type_eval/_apply_generic.py index eadc96d..02f493a 100644 --- a/typemap/type_eval/_apply_generic.py +++ b/typemap/type_eval/_apply_generic.py @@ -1,4 +1,5 @@ import annotationlib +import contextlib import dataclasses import inspect import sys @@ -204,6 +205,17 @@ def make_func( EXCLUDED_ATTRIBUTES = typing.EXCLUDED_ATTRIBUTES - {'__init__'} # type: ignore[attr-defined] +@contextlib.contextmanager +def _make_typevar_getattr_stuck(): + """Catch AttributeError on typing.TypeVar and turn it into StuckError.""" + try: + yield + except AttributeError as e: + if str(e).startswith("'typing.TypeVar'"): + raise _eval_typing.StuckException + raise + + def get_annotations( obj: object, args: Mapping[str, object], @@ -227,7 +239,8 @@ def get_annotations( globs = af.__globals__ ff = types.FunctionType(af.__code__, globs, af.__name__, None, closure) - rr = ff(annotationlib.Format.VALUE) + with _make_typevar_getattr_stuck(): + rr = ff(annotationlib.Format.VALUE) elif annos_ok and (rr := getattr(obj, "__annotations__", None)): globs = {} if mod := sys.modules.get(obj.__module__): @@ -258,13 +271,15 @@ def get_annotations( for k, v in rr.items(): # Eval strings if isinstance(v, str): - v = eval(v, globs, args) + with _make_typevar_getattr_stuck(): + v = eval(v, globs, args) # Handle cases where annotation is explicitly a string, # e.g.: # class Foo[X]: # x: "Foo[X | None]" if isinstance(v, str): - v = eval(v, globs, args) + with _make_typevar_getattr_stuck(): + v = eval(v, globs, args) rr[k] = v return rr @@ -275,9 +290,18 @@ def _resolved_function_signature(func, args): import typemap.typing as nt + # We need to grab the signature and don't care about annotations, + # since we will be replacing those immediately. + # We use format=FORWARDREF to swallow all problems, and we disable + # the special_form_evaluator on top of that mostly for performance. + # + # (Before we added dot notation for Member, disabling the + # special_form_evaluator was sufficient.) token = nt.special_form_evaluator.set(None) try: - sig = inspect.signature(func) + sig = inspect.signature( + func, annotation_format=annotationlib.Format.FORWARDREF + ) finally: nt.special_form_evaluator.reset(token) diff --git a/typemap/type_eval/_eval_call.py b/typemap/type_eval/_eval_call.py index b223078..fa24887 100644 --- a/typemap/type_eval/_eval_call.py +++ b/typemap/type_eval/_eval_call.py @@ -1,3 +1,4 @@ +import annotationlib import enum import inspect import types @@ -38,7 +39,12 @@ def _get_bound_type_args( arg_types: tuple[RtType, ...], kwarg_types: dict[str, RtType], ) -> dict[str, RtType]: - sig = inspect.signature(func) + # Run in ForwardRef mode so that if one of the arguments or if the + # return value crashes due to a bad attribution projection or + # something, the others will survive it. + sig = inspect.signature( + func, annotation_format=annotationlib.Format.FORWARDREF + ) bound = sig.bind(*arg_types, **kwarg_types) diff --git a/typemap/typing.py b/typemap/typing.py index 9012087..486481f 100644 --- a/typemap/typing.py +++ b/typemap/typing.py @@ -323,7 +323,7 @@ def __iter__(self): if evaluator: return evaluator(self) else: - return iter(typing.TypeVarTuple("_IterDummy")) + return iter(()) @_SpecialForm