From 73cd90680136875546fcb1555cf432ad78e2c773 Mon Sep 17 00:00:00 2001 From: dnwpark Date: Thu, 29 Jan 2026 13:11:30 -0800 Subject: [PATCH 1/2] Rename GetAttr to GetMemberType. --- pep.rst | 11 ++++++----- tests/test_fastapilike_2.py | 4 ++-- tests/test_qblike.py | 6 +++--- tests/test_qblike_2.py | 8 ++++---- tests/test_type_eval.py | 18 +++++++++--------- typemap/type_eval/_eval_operators.py | 6 +++--- typemap/typing.py | 12 ++++++------ 7 files changed, 33 insertions(+), 32 deletions(-) diff --git a/pep.rst b/pep.rst index 433d912..6d1c8b9 100644 --- a/pep.rst +++ b/pep.rst @@ -510,8 +510,8 @@ Basic operators cannot be. -* ``GetAttr[T, S: Literal[str]]``: Extract the type of the member - named ``S`` from the class ``T``. +* ``GetMemberType[T, S: Literal[str]]``: Extract the type of the + member named ``S`` from the class ``T``. * ``Length[T: tuple]`` - get the length of a tuple as an int literal (or ``Literal[None]`` if it is unbounded) @@ -837,7 +837,7 @@ type-annotated attribute of ``K``, while calling ``NewProtocol`` with ``GetName`` is a getter operator that fetches the name of a ``Member`` as a literal type--all of these mechanisms lean very heavily on literal types. -``GetAttr`` gets the type of an attribute from a class. +``GetMemberType`` gets the type of an attribute from a class. :: @@ -850,7 +850,7 @@ as a literal type--all of these mechanisms lean very heavily on literal types. *[ Member[ GetName[c], - ConvertField[GetAttr[ModelT, GetName[c]]], + ConvertField[GetMemberType[ModelT, GetName[c]]], ] for c in Iter[Attrs[K]] ] @@ -1119,7 +1119,8 @@ We could do potentially better but it would require more meachinery. * ``Member[T]``, when statically checking a type alias, could be treated as having some type like ``tuple[Member[KeyOf[T], object, str, ..., ...], ...]`` -* ``GetAttr[T, S: KeyOf[T]]`` - but this isn't supported yet. TS supports it. +* ``GetMemberType[T, S: KeyOf[T]]`` - but this isn't supported yet. + TS supports it. * We would also need to do context sensitive type bound inference diff --git a/tests/test_fastapilike_2.py b/tests/test_fastapilike_2.py index 3c0ac80..c3e35c9 100644 --- a/tests/test_fastapilike_2.py +++ b/tests/test_fastapilike_2.py @@ -18,7 +18,7 @@ IsSub, FromUnion, GetArg, - GetAttr, + GetMemberType, GetType, GetName, GetQuals, @@ -46,7 +46,7 @@ class Field[T: FieldArgs](InitField[T]): #### # TODO: Should this go into the stdlib? -type GetFieldItem[T: InitField, K] = GetAttr[ +type GetFieldItem[T: InitField, K] = GetMemberType[ GetArg[T, InitField, Literal[0]], K ] diff --git a/tests/test_qblike.py b/tests/test_qblike.py index 13b7393..dfe0f25 100644 --- a/tests/test_qblike.py +++ b/tests/test_qblike.py @@ -16,7 +16,7 @@ GetType, Member, GetName, - GetAttr, + GetMemberType, GetArg, ) @@ -49,7 +49,7 @@ def select[K: BaseTypedDict]( *[ Member[ GetName[c], - FilterLinks[GetAttr[A, GetName[c]]], + FilterLinks[GetMemberType[A, GetName[c]]], ] for c in Iter[Attrs[K]] ] @@ -118,7 +118,7 @@ class select[...]: z: tests.test_qblike.Link[tests.test_qblike.PropsOnly[tests.test_qblike.Tgt]] """) - res = eval_typing(GetAttr[ret, Literal["z"]]) + res = eval_typing(GetMemberType[ret, Literal["z"]]) tgt = res.__args__[0] # XXX: this should probably be pre-evaluated already? tgt = eval_typing(tgt) diff --git a/tests/test_qblike_2.py b/tests/test_qblike_2.py index e75612f..7eaa820 100644 --- a/tests/test_qblike_2.py +++ b/tests/test_qblike_2.py @@ -12,7 +12,7 @@ GetType, Member, GetName, - GetAttr, + GetMemberType, GetArg, ) @@ -58,7 +58,7 @@ class MultiLink[T](Link[T]): ``GetName`` is a getter operator that fetches the name of a ``Member`` as a literal type--all of these mechanisms lean very heavily on literal types. -``GetAttr`` gets the type of an attribute from a class. +``GetMemberType`` gets the type of an attribute from a class. """ @@ -72,7 +72,7 @@ def select[ModelT, K: BaseTypedDict]( *[ Member[ GetName[c], - ConvertField[GetAttr[ModelT, GetName[c]]], + ConvertField[GetMemberType[ModelT, GetName[c]]], ] for c in Iter[Attrs[K]] ] @@ -198,7 +198,7 @@ class select[...]: posts: list[tests.test_qblike_2.PropsOnly[tests.test_qblike_2.Post]] """) - res = eval_typing(GetAttr[ret, Literal["posts"]]) + res = eval_typing(GetMemberType[ret, Literal["posts"]]) tgt = res.__args__[0] # XXX: this should probably be pre-evaluated already? fmt = format_helper.format_class(tgt) diff --git a/tests/test_type_eval.py b/tests/test_type_eval.py index 8397f56..cdf1047 100644 --- a/tests/test_type_eval.py +++ b/tests/test_type_eval.py @@ -25,7 +25,7 @@ GenericCallable, GetArg, GetArgs, - GetAttr, + GetMemberType, GetName, GetType, GetAnnotations, @@ -198,27 +198,27 @@ def test_eval_arg_order(): def test_type_getattr_union_1(): - d = eval_typing(GetAttr[TA | TB, Literal["x"]]) + d = eval_typing(GetMemberType[TA | TB, Literal["x"]]) assert d == int | str def test_type_getattr_union_2(): - d = eval_typing(GetAttr[TA, Literal["x"] | Literal["y"]]) + d = eval_typing(GetMemberType[TA, Literal["x"] | Literal["y"]]) assert d == int | list[float] def test_type_getattr_union_3(): - d = eval_typing(GetAttr[TA | TB, Literal["x"] | Literal["y"]]) + d = eval_typing(GetMemberType[TA | TB, Literal["x"] | Literal["y"]]) assert d == int | list[float] | str | list[object] def test_type_getattr_union_4(): - d = eval_typing(GetAttr[TA, Literal["x", "y"]]) + d = eval_typing(GetMemberType[TA, Literal["x", "y"]]) assert d == int | list[float] def test_type_getattr_union_5(): - d = eval_typing(GetAttr[TA, Literal["x", "y"] | Literal["z"]]) + d = eval_typing(GetMemberType[TA, Literal["x", "y"] | Literal["z"]]) assert d == int | list[float] | TB @@ -1537,15 +1537,15 @@ class AnnoTest: def test_type_eval_annotated_02(): - res = eval_typing(IsSub[GetAttr[AnnoTest, Literal["a"]], int]) + res = eval_typing(IsSub[GetMemberType[AnnoTest, Literal["a"]], int]) assert res == _BoolLiteral[True] def test_type_eval_annotated_03(): - res = eval_typing(Uppercase[GetAttr[AnnoTest, Literal["b"]]]) + res = eval_typing(Uppercase[GetMemberType[AnnoTest, Literal["b"]]]) assert res == Literal["TEST"] def test_type_eval_annotated_04(): - res = eval_typing(GetAnnotations[GetAttr[AnnoTest, Literal["b"]]]) + res = eval_typing(GetAnnotations[GetMemberType[AnnoTest, Literal["b"]]]) assert res == Literal["blah"] diff --git a/typemap/type_eval/_eval_operators.py b/typemap/type_eval/_eval_operators.py index 1b5ae7e..8960abb 100644 --- a/typemap/type_eval/_eval_operators.py +++ b/typemap/type_eval/_eval_operators.py @@ -25,7 +25,7 @@ GetAnnotations, GetArg, GetArgs, - GetAttr, + GetMemberType, InitField, IsSubSimilar, IsSubtype, @@ -705,9 +705,9 @@ def _eval_FromUnion(tp, *, ctx): ################################################################## -@type_eval.register_evaluator(GetAttr) +@type_eval.register_evaluator(GetMemberType) @_lift_over_unions -def _eval_GetAttr(tp, prop, *, ctx): +def _eval_GetMemberType(tp, prop, *, ctx): # XXX: extras? name = _from_literal(prop) hints = { diff --git a/typemap/typing.py b/typemap/typing.py index 056ab8e..8ec62ac 100644 --- a/typemap/typing.py +++ b/typemap/typing.py @@ -112,11 +112,11 @@ class Param[N: str | None, T, Q: ParamQuals = typing.Never]: type KwargsParam[T] = Param[Literal[None], T, Literal["**"]] -type GetName[T: Member | Param] = GetAttr[T, Literal["name"]] -type GetType[T: Member | Param] = GetAttr[T, Literal["typ"]] -type GetQuals[T: Member | Param] = GetAttr[T, Literal["quals"]] -type GetInit[T: Member] = GetAttr[T, Literal["init"]] -type GetDefiner[T: Member] = GetAttr[T, Literal["definer"]] +type GetName[T: Member | Param] = GetMemberType[T, Literal["name"]] +type GetType[T: Member | Param] = GetMemberType[T, Literal["typ"]] +type GetQuals[T: Member | Param] = GetMemberType[T, Literal["quals"]] +type GetInit[T: Member] = GetMemberType[T, Literal["init"]] +type GetDefiner[T: Member] = GetMemberType[T, Literal["definer"]] class Attrs[T]: @@ -131,7 +131,7 @@ class FromUnion[T]: pass -class GetAttr[Lhs, Prop]: +class GetMemberType[Lhs, Prop]: pass From 6325277aab04256acb54311d844983e0f74a3c89 Mon Sep 17 00:00:00 2001 From: dnwpark Date: Thu, 29 Jan 2026 13:07:53 -0800 Subject: [PATCH 2/2] Add GetMember. --- pep.rst | 3 +++ tests/test_type_eval.py | 16 ++++++++++++ typemap/type_eval/_eval_operators.py | 37 ++++++++++++++++++++-------- typemap/typing.py | 4 +++ 4 files changed, 50 insertions(+), 10 deletions(-) diff --git a/pep.rst b/pep.rst index 6d1c8b9..b2a679d 100644 --- a/pep.rst +++ b/pep.rst @@ -540,6 +540,9 @@ Object inspection * ``Attrs[T]``: like ``Members[T]`` but only returns attributes (not methods). +* ``GetMember[T, S: Literal[str]]``: Produces a ``Member`` type for the + member named ``S`` from the class ``T``. + * ``Member[N: Literal[str], T, Q: MemberQuals, Init, D]``: ``Member``, is a simple type, not an operator, that is used to describe members of classes. Its type parameters encode the information about each diff --git a/tests/test_type_eval.py b/tests/test_type_eval.py index cdf1047..9b239d6 100644 --- a/tests/test_type_eval.py +++ b/tests/test_type_eval.py @@ -25,6 +25,7 @@ GenericCallable, GetArg, GetArgs, + GetMember, GetMemberType, GetName, GetType, @@ -379,6 +380,21 @@ def test_type_from_union_06(): assert _is_generic_permutation(n, tuple[int, list[IntTree]]) +def test_getmember_01(): + d = eval_typing(GetMember[TA, Literal["x"]]) + assert d == Member[Literal["x"], int, Never, Never, TA] + d = eval_typing(GetMemberType[TA, Literal["a"]]) + assert d == Never + + d = eval_typing(GetMember[TA | TB, Literal["x"]]) + assert d == ( + Member[Literal["x"], int, Never, Never, TA] + | Member[Literal["x"], str, Never, Never, TB] + ) + d = eval_typing(GetMember[TA | TB, Literal[""]]) + assert d == Never + + def test_getarg_never(): d = eval_typing(GetArg[Never, object, Literal[0]]) assert d is Never diff --git a/typemap/type_eval/_eval_operators.py b/typemap/type_eval/_eval_operators.py index 8960abb..df2ac67 100644 --- a/typemap/type_eval/_eval_operators.py +++ b/typemap/type_eval/_eval_operators.py @@ -25,6 +25,7 @@ GetAnnotations, GetArg, GetArgs, + GetMember, GetMemberType, InitField, IsSubSimilar, @@ -657,19 +658,20 @@ def _get_function_hint_namespaces(func, receiver_type=None): return globalns, localns +def _hint_to_member(n, t, qs, init, d, *, ctx): + return Member[ + typing.Literal[n], + _eval_types(t, ctx), + _mk_literal_union(*qs), + init, + d, + ] + + def _hints_to_members(hints, ctx): """Convert a hints dictionary to a tuple of Member types.""" return tuple[ - *[ - Member[ - typing.Literal[n], - _eval_types(t, ctx), - _mk_literal_union(*qs), - init, - d, - ] - for n, (t, qs, init, d) in hints.items() - ] + *[_hint_to_member(n, *hint, ctx=ctx) for n, hint in hints.items()] ] @@ -690,6 +692,21 @@ def _eval_Members(tp, *, ctx): return _hints_to_members(hints, ctx) +@type_eval.register_evaluator(GetMember) +@_lift_over_unions +def _eval_GetMember(tp, prop, *, ctx): + # XXX: extras? + name = _from_literal(prop) + hints = { + **get_annotated_type_hints(tp, include_extras=True), + **get_annotated_method_hints(tp), + } + if name in hints: + return _hint_to_member(name, *hints[name], ctx=ctx) + else: + return typing.Never + + ################################################################## diff --git a/typemap/typing.py b/typemap/typing.py index 8ec62ac..9db378d 100644 --- a/typemap/typing.py +++ b/typemap/typing.py @@ -131,6 +131,10 @@ class FromUnion[T]: pass +class GetMember[Lhs, Prop]: + pass + + class GetMemberType[Lhs, Prop]: pass