diff --git a/pep.rst b/pep.rst index 559ac6c..30f1e2f 100644 --- a/pep.rst +++ b/pep.rst @@ -580,6 +580,13 @@ Basic operators * ``GetMemberType[T, S: Literal[str]]``: Extract the type of the member named ``S`` from the class ``T``. + +* ``GetSpecialAttr[T: type, Attr: Literal[str]]``: Extract the value + of special attribute named ``Attr`` from the class ``T``. Valid + attributes are ``__name__``, ``__module__``, and ``__qualname__``. + Returns the value as a ``Literal[str]``. + + * ``Length[T: tuple]`` - get the length of a tuple as an int literal (or ``Literal[None]`` if it is unbounded) diff --git a/tests/test_type_eval.py b/tests/test_type_eval.py index 9b239d6..44e3b1b 100644 --- a/tests/test_type_eval.py +++ b/tests/test_type_eval.py @@ -28,6 +28,7 @@ GetMember, GetMemberType, GetName, + GetSpecialAttr, GetType, GetAnnotations, IsSub, @@ -990,6 +991,85 @@ class Container2[T]: ... assert eval_typing(GetArg[t, Container, Literal[1]]) == Never +class OuterType: + class InnerType: + pass + + +def test_eval_typename_01(): + d = eval_typing(GetSpecialAttr[int, Literal["__name__"]]) + assert d == Literal["int"] + d = eval_typing(GetSpecialAttr[str, Literal["__name__"]]) + assert d == Literal["str"] + d = eval_typing(GetSpecialAttr[list[int], Literal["__name__"]]) + assert d == Literal["list"] + d = eval_typing(GetSpecialAttr[list[str], Literal["__name__"]]) + assert d == Literal["list"] + + class C: + pass + + d = eval_typing(GetSpecialAttr[OuterType, Literal["__name__"]]) + assert d == Literal["OuterType"] + d = eval_typing(GetSpecialAttr[OuterType.InnerType, Literal["__name__"]]) + assert d == Literal["InnerType"] + d = eval_typing(GetSpecialAttr[C, Literal["__name__"]]) + assert d == Literal["C"] + + d = eval_typing(GetSpecialAttr[GetA1[int, str], Literal["__name__"]]) + assert d == Literal["int"] + + +def test_eval_typename_02(): + d = eval_typing(GetSpecialAttr[int, Literal["__module__"]]) + assert d == Literal["builtins"] + d = eval_typing(GetSpecialAttr[str, Literal["__module__"]]) + assert d == Literal["builtins"] + d = eval_typing(GetSpecialAttr[list[int], Literal["__module__"]]) + assert d == Literal["builtins"] + d = eval_typing(GetSpecialAttr[list[str], Literal["__module__"]]) + assert d == Literal["builtins"] + + class C: + pass + + d = eval_typing(GetSpecialAttr[OuterType, Literal["__module__"]]) + assert d == Literal["tests.test_type_eval"] + d = eval_typing(GetSpecialAttr[OuterType.InnerType, Literal["__module__"]]) + assert d == Literal["tests.test_type_eval"] + d = eval_typing(GetSpecialAttr[C, Literal["__module__"]]) + assert d == Literal["tests.test_type_eval"] + + d = eval_typing(GetSpecialAttr[GetA1[int, str], Literal["__module__"]]) + assert d == Literal["builtins"] + + +def test_eval_typename_03(): + d = eval_typing(GetSpecialAttr[int, Literal["__qualname__"]]) + assert d == Literal["int"] + d = eval_typing(GetSpecialAttr[str, Literal["__qualname__"]]) + assert d == Literal["str"] + d = eval_typing(GetSpecialAttr[list[int], Literal["__qualname__"]]) + assert d == Literal["list"] + d = eval_typing(GetSpecialAttr[list[str], Literal["__qualname__"]]) + assert d == Literal["list"] + + class C: + pass + + d = eval_typing(GetSpecialAttr[OuterType, Literal["__qualname__"]]) + assert d == Literal["OuterType"] + d = eval_typing( + GetSpecialAttr[OuterType.InnerType, Literal["__qualname__"]] + ) + assert d == Literal["OuterType.InnerType"] + d = eval_typing(GetSpecialAttr[C, Literal["__qualname__"]]) + assert d == Literal["test_eval_typename_03..C"] + + d = eval_typing(GetSpecialAttr[GetA1[int, str], Literal["__qualname__"]]) + assert d == Literal["int"] + + type _Works[Ts, I] = Literal[True] type Works[Ts] = _Works[Ts, Length[Ts]] diff --git a/typemap/type_eval/_eval_operators.py b/typemap/type_eval/_eval_operators.py index df2ac67..38f4b1a 100644 --- a/typemap/type_eval/_eval_operators.py +++ b/typemap/type_eval/_eval_operators.py @@ -27,6 +27,7 @@ GetArgs, GetMember, GetMemberType, + GetSpecialAttr, InitField, IsSubSimilar, IsSubtype, @@ -923,6 +924,28 @@ def _eval_GetArgs(tp, base, *, ctx) -> typing.Any: return tuple[*args] # type: ignore[valid-type] +@type_eval.register_evaluator(GetSpecialAttr) +@_lift_over_unions +def _eval_GetSpecialAttr(tp, attr, *, ctx) -> typing.Any: + if not ( + _typing_inspect.is_generic_alias(attr) + and attr.__origin__ is typing.Literal + and isinstance(attr.__args__[0], str) + ): + raise TypeError( + f"Invalid type argument to GetSpecialAttr: " + f"{attr} is not a string Literal" + ) + if attr.__args__[0] == "__name__": + return typing.Literal[tp.__name__] + elif attr.__args__[0] == "__module__": + return typing.Literal[tp.__module__] + elif attr.__args__[0] == "__qualname__": + return typing.Literal[tp.__qualname__] + else: + return typing.Never + + @type_eval.register_evaluator(GetAnnotations) def _eval_GetAnnotations(tp, *, ctx) -> typing.Any: tp = _eval_types(tp, ctx=ctx) diff --git a/typemap/typing.py b/typemap/typing.py index 9db378d..3fe980d 100644 --- a/typemap/typing.py +++ b/typemap/typing.py @@ -147,6 +147,10 @@ class GetArgs[Tp, Base]: pass +class GetSpecialAttr[T: type, Attr: str]: + pass + + class Length[S: tuple]: pass