From f934acc6008e79fbae70101aeafad0031c306a11 Mon Sep 17 00:00:00 2001 From: "Michael J. Sullivan" Date: Mon, 22 Dec 2025 15:03:06 -0800 Subject: [PATCH 1/9] Expand note about Unpack for **kwargs --- spec-draft.rst | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/spec-draft.rst b/spec-draft.rst index e5279b4..f9380df 100644 --- a/spec-draft.rst +++ b/spec-draft.rst @@ -1,8 +1,17 @@ A minor proposal that could be split out maybe: -Supporting ``Unpack`` of typevars for ``*kwargs`` +Supporting ``Unpack`` of typevars for ``**kwargs``:: + def f[K: BaseTypedDict](**kwargs: Unpack[K]) -> K: + return kwargs +Here ``BaseTypedDict`` is defined as:: + class BaseTypedDict(typing.TypedDict): + pass + +But any typeddict would be allowed there. (Or, maybe we should allow ``dict``?) + +This is basically a combination of "PEP 692 – Using TypedDict for more precise **kwargs typing" and the behavior of ``Unpack`` for ``*args`` from "PEP 646 – Variadic Generics". ----------------------------------------------------------------------- From 483a48183de37cba1b3a14d2acd04e78dd83ac5e Mon Sep 17 00:00:00 2001 From: "Michael J. Sullivan" Date: Mon, 22 Dec 2025 17:04:16 -0800 Subject: [PATCH 2/9] Lay groundwork for better Members handling --- spec-draft.rst | 1 + tests/test_type_dir.py | 3 ++ typemap/type_eval/_eval_operators.py | 68 ++++++++++++++++++---------- typemap/type_eval/_subsim.py | 4 +- 4 files changed, 51 insertions(+), 25 deletions(-) diff --git a/spec-draft.rst b/spec-draft.rst index f9380df..b79c774 100644 --- a/spec-draft.rst +++ b/spec-draft.rst @@ -92,6 +92,7 @@ Big Q: what should be an error and what should return Never? --- * ``GetAttr[T, S: Literal[str]]`` + TODO: How should GetAttr interact with descriptors/classmethod? I am leaning towards it should apply the descriptor... # TODO: how to deal with special forms like Callable and tuple[T, ...] diff --git a/tests/test_type_dir.py b/tests/test_type_dir.py index 1f01146..0bea75a 100644 --- a/tests/test_type_dir.py +++ b/tests/test_type_dir.py @@ -251,3 +251,6 @@ def test_type_dir_7(): def test_type_dir_8(): d = eval_typing(BaseArg[Final]) assert d is int + + +# TODO: add a test for _lift_over_unions producing Never diff --git a/typemap/type_eval/_eval_operators.py b/typemap/type_eval/_eval_operators.py index ad5ff46..63b817b 100644 --- a/typemap/type_eval/_eval_operators.py +++ b/typemap/type_eval/_eval_operators.py @@ -40,7 +40,7 @@ def _from_literal(val, ctx): def get_annotated_type_hints(cls, **kwargs): - """Get the type hints for a cls annotated with definition site. + """Get the type hints/quals for a cls annotated with definition site. This traverses the mro and finds the definition site for each annotation. """ @@ -51,7 +51,8 @@ def get_annotated_type_hints(cls, **kwargs): continue for k in acls.__annotations__: if k not in hints: - hints[k] = ohints[k], acls + # XXX: TODO: Strip ClassVar/Final + hints[k] = ohints[k], (), acls # Stop early if we are done. if len(hints) == len(ohints): @@ -59,6 +60,24 @@ def get_annotated_type_hints(cls, **kwargs): return hints +def get_annotated_method_hints(tp): + hints = {} + # XXX: traverse mro + for name, attr in tp.__dict__.items(): + if isinstance(attr, (types.FunctionType, types.MethodType)): + if attr is typing._no_init_or_replace_init: + continue + + # XXX: populate the source field + hints[name] = ( + _function_type(attr, is_method=True), + ("ClassVar",), + typing.Never, + ) + + return hints + + def _union_elems(tp, ctx): tp = _eval_types(tp, ctx) if isinstance(tp, types.UnionType): @@ -69,13 +88,27 @@ def _union_elems(tp, ctx): return (tp,) +# TODO: Need to be able to do this in type system! +def _mk_union(*parts): + if not parts: + return typing.Never + else: + return typing.Union[*parts] + + +def _mk_literal_union(*parts): + if not parts: + return typing.Never + else: + return typing.Literal[*parts] + + def _lift_over_unions(func): @functools.wraps(func) def wrapper(*args, ctx): args2 = [_union_elems(x, ctx) for x in args] - # XXX: Never parts = [func(*x, ctx=ctx) for x in itertools.product(*args2)] - return typing.Union[*parts] + return _mk_union(*parts) return wrapper @@ -167,8 +200,8 @@ def _eval_Attrs(tp, *, ctx): return tuple[ *[ - Member[typing.Literal[n], t, typing.Never, d] - for n, (t, d) in hints.items() + Member[typing.Literal[n], t, _mk_literal_union(*qs), d] + for n, (t, qs, d) in hints.items() ] ] @@ -176,27 +209,16 @@ def _eval_Attrs(tp, *, ctx): @type_eval.register_evaluator(Members) @_lift_over_unions def _eval_Members(tp, *, ctx): - hints = get_annotated_type_hints(tp, include_extras=True) + hints = { + **get_annotated_type_hints(tp, include_extras=True), + **get_annotated_method_hints(tp), + } attrs = [ - Member[typing.Literal[n], t, typing.Never, d] - for n, (t, d) in hints.items() + Member[typing.Literal[n], t, _mk_literal_union(*qs), d] + for n, (t, qs, d) in hints.items() ] - for name, attr in tp.__dict__.items(): - if isinstance(attr, (types.FunctionType, types.MethodType)): - if attr is typing._no_init_or_replace_init: - continue - - # XXX: populate the source field - attrs.append( - Member[ - typing.Literal[name], - _function_type(attr, is_method=True), - typing.Literal["ClassVar"], - ] - ) - return tuple[*attrs] diff --git a/typemap/type_eval/_subsim.py b/typemap/type_eval/_subsim.py index 8304d8d..22912fc 100644 --- a/typemap/type_eval/_subsim.py +++ b/typemap/type_eval/_subsim.py @@ -14,9 +14,9 @@ def issubsimilar(lhs: typing.Any, rhs: typing.Any) -> bool: # formats the two-conditional chains in an unconscionably bad way. # Unions first - if _typing_inspect.is_union_type(rhs): + if _typing_inspect.is_union_type(rhs) or rhs is typing.Never: return any(issubsimilar(lhs, r) for r in typing.get_args(rhs)) - elif _typing_inspect.is_union_type(lhs): + elif _typing_inspect.is_union_type(lhs) or lhs is typing.Never: return all(issubsimilar(t, rhs) for t in typing.get_args(lhs)) # For _EvalProxy's just blow through them, since we don't yet care From 96a6735696a11eff2ce5b5c80cb4d7ebdab449bf Mon Sep 17 00:00:00 2001 From: "Michael J. Sullivan" Date: Mon, 22 Dec 2025 17:17:30 -0800 Subject: [PATCH 3/9] Look at MRO properly for Members --- tests/test_type_dir.py | 2 +- typemap/type_eval/_eval_operators.py | 23 +++++++++++------------ 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/tests/test_type_dir.py b/tests/test_type_dir.py index 0bea75a..f6579a7 100644 --- a/tests/test_type_dir.py +++ b/tests/test_type_dir.py @@ -244,7 +244,7 @@ def test_type_dir_7(): typemap.typing.Param[typing.Literal['self'], typing.Any, typing.Never], \ typemap.typing.Param[typing.Literal['a'], int | None, typing.Never], \ typemap.typing.Param[typing.Literal['b'], int, typing.Literal['=']]], \ -dict[str, int]], typing.Literal['ClassVar'], typing.Never]" +dict[str, int]], typing.Literal['ClassVar'], tests.test_type_dir.Final]" ) diff --git a/typemap/type_eval/_eval_operators.py b/typemap/type_eval/_eval_operators.py index 63b817b..3d67c41 100644 --- a/typemap/type_eval/_eval_operators.py +++ b/typemap/type_eval/_eval_operators.py @@ -62,18 +62,17 @@ def get_annotated_type_hints(cls, **kwargs): def get_annotated_method_hints(tp): hints = {} - # XXX: traverse mro - for name, attr in tp.__dict__.items(): - if isinstance(attr, (types.FunctionType, types.MethodType)): - if attr is typing._no_init_or_replace_init: - continue - - # XXX: populate the source field - hints[name] = ( - _function_type(attr, is_method=True), - ("ClassVar",), - typing.Never, - ) + for ptp in reversed(tp.mro()): + for name, attr in ptp.__dict__.items(): + if isinstance(attr, (types.FunctionType, types.MethodType)): + if attr is typing._no_init_or_replace_init: + continue + + hints[name] = ( + _function_type(attr, is_method=True), + ("ClassVar",), + ptp, + ) return hints From 9b991ee8e763298d84777a7803d58f0a288a0746 Mon Sep 17 00:00:00 2001 From: "Michael J. Sullivan" Date: Mon, 22 Dec 2025 17:28:59 -0800 Subject: [PATCH 4/9] Fix lifting type operations over Never --- tests/test_type_dir.py | 3 --- tests/test_type_eval.py | 29 ++++++++++++++++++---------- typemap/type_eval/_eval_operators.py | 24 +++++++++++------------ 3 files changed, 31 insertions(+), 25 deletions(-) diff --git a/tests/test_type_dir.py b/tests/test_type_dir.py index f6579a7..19b82e9 100644 --- a/tests/test_type_dir.py +++ b/tests/test_type_dir.py @@ -251,6 +251,3 @@ def test_type_dir_7(): def test_type_dir_8(): d = eval_typing(BaseArg[Final]) assert d is int - - -# TODO: add a test for _lift_over_unions producing Never diff --git a/tests/test_type_eval.py b/tests/test_type_eval.py index a7dcea4..acd15fa 100644 --- a/tests/test_type_eval.py +++ b/tests/test_type_eval.py @@ -1,25 +1,26 @@ import textwrap import unittest -from typing import Literal +from typing import Literal, Never +from typemap.type_eval import eval_typing from typemap.typing import ( - NewProtocol, - Member, + Attrs, + FromUnion, + GetArg, GetAttr, GetName, GetType, - Iter, - Attrs, Is, - Uppercase, + Iter, + Member, + NewProtocol, StrConcat, StrSlice, + Uppercase, ) -from typemap.type_eval import eval_typing from . import format_helper - type A[T] = T | None | Literal[False] type B = A[int] @@ -164,9 +165,17 @@ def test_type_strings_6(): def test_type_asdf(): - from typemap.typing import FromUnion - d = eval_typing(FromUnion[int | bool]) arg = FromUnion[int | str] d = eval_typing(arg) assert d == tuple[int, str] or d == tuple[str, int] + + +def test_getarg_never(): + d = eval_typing(GetArg[Never, object, 0]) + assert d is Never + + +def test_uppercase_never(): + d = eval_typing(Uppercase[Never]) + assert d is Never diff --git a/typemap/type_eval/_eval_operators.py b/typemap/type_eval/_eval_operators.py index 3d67c41..382e145 100644 --- a/typemap/type_eval/_eval_operators.py +++ b/typemap/type_eval/_eval_operators.py @@ -7,28 +7,26 @@ from typemap import type_eval from typemap.type_eval import _typing_inspect from typemap.type_eval._eval_typing import _eval_types - from typemap.typing import ( Attrs, - Iter, - IsSubtype, + Capitalize, + FromUnion, + GetArg, + GetAttr, IsSubSimilar, + IsSubtype, + Iter, + Lowercase, Member, Members, NewProtocol, Param, - FromUnion, - GetArg, - GetAttr, - Capitalize, - Uncapitalize, - Uppercase, - Lowercase, StrConcat, StrSlice, + Uncapitalize, + Uppercase, ) - ################################################################## @@ -79,7 +77,9 @@ def get_annotated_method_hints(tp): def _union_elems(tp, ctx): tp = _eval_types(tp, ctx) - if isinstance(tp, types.UnionType): + if tp is typing.Never: + return () + elif isinstance(tp, types.UnionType): return tuple(y for x in tp.__args__ for y in _union_elems(x, ctx)) elif _typing_inspect.is_literal(tp) and len(tp.__args__) > 1: return tuple(typing.Literal[x] for x in tp.__args__) From ae9b23903f6bdb76a5511701bcc11a3ec6decbed Mon Sep 17 00:00:00 2001 From: "Michael J. Sullivan" Date: Mon, 5 Jan 2026 10:04:06 -0800 Subject: [PATCH 5/9] Fix Is[Never, Never] and be a bit less hacky --- tests/test_type_eval.py | 5 +++++ typemap/type_eval/_subsim.py | 13 +++++++------ 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/tests/test_type_eval.py b/tests/test_type_eval.py index acd15fa..c0ad1f2 100644 --- a/tests/test_type_eval.py +++ b/tests/test_type_eval.py @@ -179,3 +179,8 @@ def test_getarg_never(): def test_uppercase_never(): d = eval_typing(Uppercase[Never]) assert d is Never + + +def test_never_is(): + d = eval_typing(Is[Never, Never]) + assert d is True diff --git a/typemap/type_eval/_subsim.py b/typemap/type_eval/_subsim.py index 22912fc..5e965d3 100644 --- a/typemap/type_eval/_subsim.py +++ b/typemap/type_eval/_subsim.py @@ -14,9 +14,14 @@ def issubsimilar(lhs: typing.Any, rhs: typing.Any) -> bool: # formats the two-conditional chains in an unconscionably bad way. # Unions first - if _typing_inspect.is_union_type(rhs) or rhs is typing.Never: + if lhs is typing.Never: + return True + elif rhs is typing.Never: + return False + + elif _typing_inspect.is_union_type(rhs): return any(issubsimilar(lhs, r) for r in typing.get_args(rhs)) - elif _typing_inspect.is_union_type(lhs) or lhs is typing.Never: + elif _typing_inspect.is_union_type(lhs): return all(issubsimilar(t, rhs) for t in typing.get_args(lhs)) # For _EvalProxy's just blow through them, since we don't yet care @@ -75,10 +80,6 @@ def issubsimilar(lhs: typing.Any, rhs: typing.Any) -> bool: elif _typing_inspect.is_type_var(lhs): return lhs is rhs - # TODO: and we will we need to infer variance ourselves with the new syntax - - # TODO: Protocols??? - # Check behavior? # TODO: Annotated # TODO: tuple From 17155d1e86eca8d6c110e27e52eb7d837050a054 Mon Sep 17 00:00:00 2001 From: "Michael J. Sullivan" Date: Mon, 5 Jan 2026 10:41:22 -0800 Subject: [PATCH 6/9] Add and implement Length --- spec-draft.rst | 4 ++-- tests/test_type_eval.py | 34 +++++++++++++++++++++++++++- typemap/type_eval/_eval_operators.py | 16 +++++++++++++ typemap/typing.py | 4 ++++ 4 files changed, 55 insertions(+), 3 deletions(-) diff --git a/spec-draft.rst b/spec-draft.rst index b79c774..ccb7847 100644 --- a/spec-draft.rst +++ b/spec-draft.rst @@ -13,6 +13,7 @@ But any typeddict would be allowed there. (Or, maybe we should allow ``dict``?) This is basically a combination of "PEP 692 – Using TypedDict for more precise **kwargs typing" and the behavior of ``Unpack`` for ``*args`` from "PEP 646 – Variadic Generics". +This is potentially moderately useful on its own but is being done to support processing **kwargs with type level computation. ----------------------------------------------------------------------- @@ -96,8 +97,7 @@ Big Q: what should be an error and what should return Never? # TODO: how to deal with special forms like Callable and tuple[T, ...] -# TODO: How to do IsUnion? Might need a ``Length`` for tuples? - +* ``Length[T: tuple]`` - get the length of a tuple as an int literal (...or ``Literal[None]`` if it is unbounded) String manipulation operations for string Literal types. We can put more in, but this is what typescript has. diff --git a/tests/test_type_eval.py b/tests/test_type_eval.py index c0ad1f2..824b092 100644 --- a/tests/test_type_eval.py +++ b/tests/test_type_eval.py @@ -1,6 +1,6 @@ import textwrap import unittest -from typing import Literal, Never +from typing import Literal, Never, Tuple from typemap.type_eval import eval_typing from typemap.typing import ( @@ -12,6 +12,7 @@ GetType, Is, Iter, + Length, Member, NewProtocol, StrConcat, @@ -184,3 +185,34 @@ def test_uppercase_never(): def test_never_is(): d = eval_typing(Is[Never, Never]) assert d is True + + +def test_eval_iter(): + d = eval_typing(Iter[tuple[int, str]]) + assert tuple(d) == (int, str) + + d = eval_typing(Iter[Tuple[int, str]]) + assert tuple(d) == (int, str) + + d = eval_typing(Iter[tuple[(int, str)]]) + assert tuple(d) == (int, str) + + d = eval_typing(Iter[tuple[()]]) + assert tuple(d) == () + + +def test_eval_length_01(): + d = eval_typing(Length[tuple[int, str]]) + assert d == Literal[2] + + d = eval_typing(Length[Tuple[int, str]]) + assert d == Literal[2] + + d = eval_typing(Length[tuple[(int, str)]]) + assert d == Literal[2] + + d = eval_typing(Length[tuple[()]]) + assert d == Literal[0] + + d = eval_typing(Length[tuple[int, ...]]) + assert d == Literal[None] diff --git a/typemap/type_eval/_eval_operators.py b/typemap/type_eval/_eval_operators.py index 382e145..656e603 100644 --- a/typemap/type_eval/_eval_operators.py +++ b/typemap/type_eval/_eval_operators.py @@ -16,6 +16,7 @@ IsSubSimilar, IsSubtype, Iter, + Length, Lowercase, Member, Members, @@ -280,6 +281,21 @@ def _eval_GetArg(tp, base, idx, *, ctx) -> typing.Any: return typing.Never +@type_eval.register_evaluator(Length) +@_lift_over_unions +def _eval_Length(tp, *, ctx) -> typing.Any: + tp = _eval_types(tp, ctx) + if _typing_inspect.is_generic_alias(tp) and tp.__origin__ is tuple: + # TODO: Unpack in the middle? + if not tp.__args__ or tp.__args__[-1] is not Ellipsis: + return typing.Literal[len(tp.__args__)] + else: + return typing.Literal[None] + else: + # XXX: Or should we return Never? + raise TypeError(f"Invalid type argument to Length: {tp} is not a tuple") + + def _string_literal_op(typ, op): @_lift_over_unions def func(*args, ctx): diff --git a/typemap/typing.py b/typemap/typing.py index 9d4eccb..a9dff1d 100644 --- a/typemap/typing.py +++ b/typemap/typing.py @@ -48,6 +48,10 @@ class GetArg[Tp, Base, Idx: int]: pass +class Length[S: tuple]: + pass + + class Uppercase[S: str]: pass From 6f69cdec2e4d91b5914ecf50f1e351e4634f843b Mon Sep 17 00:00:00 2001 From: "Michael J. Sullivan" Date: Mon, 5 Jan 2026 11:12:17 -0800 Subject: [PATCH 7/9] remove a comment I think is stray --- typemap/type_eval/_eval_operators.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/typemap/type_eval/_eval_operators.py b/typemap/type_eval/_eval_operators.py index 656e603..7b74efe 100644 --- a/typemap/type_eval/_eval_operators.py +++ b/typemap/type_eval/_eval_operators.py @@ -248,8 +248,6 @@ def _get_args(tp, base, ctx) -> typing.Any: tp_head = _typing_inspect.get_head(tp) base_head = _typing_inspect.get_head(base) - # XXX: not sure this is what we want! - # at the very least we want unions I think if not tp_head or not base_head: return None From a7bc77bbe6663bb7777363b1edffdab0ad079502 Mon Sep 17 00:00:00 2001 From: "Michael J. Sullivan" Date: Mon, 5 Jan 2026 12:31:41 -0800 Subject: [PATCH 8/9] spec-draft tweak --- spec-draft.rst | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/spec-draft.rst b/spec-draft.rst index ccb7847..1171360 100644 --- a/spec-draft.rst +++ b/spec-draft.rst @@ -21,10 +21,7 @@ Grammar specification of the extensions to the type language. It's important that there be a clearly specified type language for the type-level computation---we can't just be using some poorly specified subset of all Python. -TODO: -- Look into TupleTypeVar stuff for iteration - -Big Q: what should be an error and what should return Never? +# TODO: Big Q: what should be an error and what should return Never? :: From 9b7567ac74e098d186213bac529a78917ec89463 Mon Sep 17 00:00:00 2001 From: "Michael J. Sullivan" Date: Mon, 5 Jan 2026 14:14:38 -0800 Subject: [PATCH 9/9] Handle default type params --- spec-draft.rst | 9 +- tests/test_type_eval.py | 208 ++++++++++++++++++++++++++- typemap/type_eval/_eval_operators.py | 119 ++++++++++++++- typemap/typing.py | 4 + 4 files changed, 333 insertions(+), 7 deletions(-) diff --git a/spec-draft.rst b/spec-draft.rst index 1171360..de269ef 100644 --- a/spec-draft.rst +++ b/spec-draft.rst @@ -65,8 +65,11 @@ It's important that there be a clearly specified type language for the type-leve --- -* ``GetArg[T, Base, Idx: Literal[str]]`` - returns the type argument number ``Idx`` to ``T`` when interpreted as ``Base``, or ``Never`` if it cannot be. (That is, if we have ``class A(B[C]): ...``, then ``GetArg[A, B, 0] == C`` while ``GetArg[A, A, 0] == Never``) -* ``GetArgs[T, Base]`` - returns a tuple containing all of the type arguments of ``T`` when interpreted as ``Base``, or ``Never`` if it cannot be. +* ``GetArg[T, Base, Idx: Literal[str]]`` - returns the type argument number ``Idx`` to ``T`` when interpreted as ``Base``, or ``Never`` if it cannot be. (That is, if we have ``class A(B[C]): ...``, then ``GetArg[A, B, 0] == C`` while ``GetArg[A, A, 0] == Never``). + Special forms unfortunately require some special handling: the arguments list of a ``Callable`` will be packed in a tuple, and a ``...`` will become ``SpecialFormEllipsis``. + + +* ``GetArgs[T, Base]`` - returns a tuple containing all of the type arguments of ``T`` when interpreted as ``Base``, or ``Never`` if it cannot be. (TODO: UNIMPLEMENTED) * ``FromUnion[T]`` - returns a tuple containing all of the union elements, or a 1-ary tuple containing T if it is not a union. @@ -92,8 +95,6 @@ It's important that there be a clearly specified type language for the type-leve * ``GetAttr[T, S: Literal[str]]`` TODO: How should GetAttr interact with descriptors/classmethod? I am leaning towards it should apply the descriptor... -# TODO: how to deal with special forms like Callable and tuple[T, ...] - * ``Length[T: tuple]`` - get the length of a tuple as an int literal (...or ``Literal[None]`` if it is unbounded) String manipulation operations for string Literal types. diff --git a/tests/test_type_eval.py b/tests/test_type_eval.py index 824b092..a608765 100644 --- a/tests/test_type_eval.py +++ b/tests/test_type_eval.py @@ -1,6 +1,6 @@ import textwrap import unittest -from typing import Literal, Never, Tuple +from typing import Any, Callable, Generic, List, Literal, Never, Tuple, TypeVar from typemap.type_eval import eval_typing from typemap.typing import ( @@ -15,6 +15,7 @@ Length, Member, NewProtocol, + SpecialFormEllipsis, StrConcat, StrSlice, Uppercase, @@ -177,6 +178,211 @@ def test_getarg_never(): assert d is Never +def test_eval_getarg_callable(): + # oh hmmmmmmm -- yeah maybe callable could be fully bespoke if we + # disallowed putting Callable here...! + t = Callable[[int, str], str] + args = eval_typing(GetArg[t, Callable, 0]) + assert args == tuple[int, str] + + t = Callable[int, str] + args = eval_typing(GetArg[t, Callable, 0]) + assert args == tuple[int] + + t = Callable[[], str] + args = eval_typing(GetArg[t, Callable, 0]) + assert args == tuple[()] + + t = Callable[..., str] + args = eval_typing(GetArg[t, Callable, 0]) + assert args == SpecialFormEllipsis + + t = Callable + args = eval_typing(GetArg[t, Callable, 0]) + assert args == SpecialFormEllipsis + + t = Callable + args = eval_typing(GetArg[t, Callable, 1]) + assert args == Any + + +def test_eval_getarg_tuple(): + t = tuple[int, ...] + args = eval_typing(GetArg[t, tuple, 1]) + assert args == SpecialFormEllipsis + + t = tuple + args = eval_typing(GetArg[t, tuple, 0]) + assert args == Any + + args = eval_typing(GetArg[t, tuple, 1]) + assert args == SpecialFormEllipsis + + +def test_eval_getarg_list(): + t = list[int] + arg = eval_typing(GetArg[t, list, 0]) + assert arg is int + + t = List[int] + arg = eval_typing(GetArg[t, list, 0]) + assert arg is int + + t = list + arg = eval_typing(GetArg[t, list, 0]) + assert arg == Any + + t = List + arg = eval_typing(GetArg[t, list, 0]) + assert arg == Any + + t = list[int] + arg = eval_typing(GetArg[t, List, 0]) + assert arg is int + + t = List[int] + arg = eval_typing(GetArg[t, List, 0]) + assert arg is int + + t = list + arg = eval_typing(GetArg[t, List, 0]) + assert arg == Any + + t = List + arg = eval_typing(GetArg[t, List, 0]) + assert arg == Any + + # indexing with -1 equivalent to 0 + t = list[int] + arg = eval_typing(GetArg[t, list, -1]) + assert arg is int + + t = List[int] + arg = eval_typing(GetArg[t, list, -1]) + assert arg is int + + t = list + arg = eval_typing(GetArg[t, list, -1]) + assert arg == Any + + t = List + arg = eval_typing(GetArg[t, list, -1]) + assert arg == Any + + t = list[int] + arg = eval_typing(GetArg[t, List, -1]) + assert arg is int + + t = List[int] + arg = eval_typing(GetArg[t, List, -1]) + assert arg is int + + t = list + arg = eval_typing(GetArg[t, List, -1]) + assert arg == Any + + t = List + arg = eval_typing(GetArg[t, List, -1]) + assert arg == Any + + # indexing with 1 always fails + t = list[int] + arg = eval_typing(GetArg[t, list, 1]) + assert arg == Never + + t = List[int] + arg = eval_typing(GetArg[t, list, 1]) + assert arg == Never + + t = list + arg = eval_typing(GetArg[t, list, 1]) + assert arg == Never + + t = List + arg = eval_typing(GetArg[t, list, 1]) + assert arg == Never + + t = list[int] + arg = eval_typing(GetArg[t, List, 1]) + assert arg == Never + + t = List[int] + arg = eval_typing(GetArg[t, List, 1]) + assert arg == Never + + t = list + arg = eval_typing(GetArg[t, List, 1]) + assert arg == Never + + t = List + arg = eval_typing(GetArg[t, List, 1]) + assert arg == Never + + +def test_eval_getarg_custom_01(): + class A[T]: + pass + + t = A[int] + assert eval_typing(GetArg[t, A, 0]) is int + assert eval_typing(GetArg[t, A, -1]) is int + assert eval_typing(GetArg[t, A, 1]) == Never + + t = A + assert eval_typing(GetArg[t, A, 0]) == Any + assert eval_typing(GetArg[t, A, -1]) == Any + assert eval_typing(GetArg[t, A, 1]) == Never + + +def test_eval_getarg_custom_02(): + T = TypeVar("T") + + class A(Generic[T]): + pass + + t = A[int] + assert eval_typing(GetArg[t, A, 0]) is int + assert eval_typing(GetArg[t, A, -1]) is int + assert eval_typing(GetArg[t, A, 1]) == Never + + t = A + assert eval_typing(GetArg[t, A, 0]) == Any + assert eval_typing(GetArg[t, A, -1]) == Any + assert eval_typing(GetArg[t, A, 1]) == Never + + +def test_eval_getarg_custom_03(): + class A[T = str]: + pass + + t = A[int] + assert eval_typing(GetArg[t, A, 0]) is int + assert eval_typing(GetArg[t, A, -1]) is int + assert eval_typing(GetArg[t, A, 1]) == Never + + t = A + assert eval_typing(GetArg[t, A, 0]) is str + assert eval_typing(GetArg[t, A, -1]) is str + assert eval_typing(GetArg[t, A, 1]) == Never + + +def test_eval_getarg_custom_04(): + T = TypeVar("T", default=str) + + class A(Generic[T]): + pass + + t = A[int] + assert eval_typing(GetArg[t, A, 0]) is int + assert eval_typing(GetArg[t, A, -1]) is int + assert eval_typing(GetArg[t, A, 1]) == Never + + t = A + assert eval_typing(GetArg[t, A, 0]) is str + assert eval_typing(GetArg[t, A, -1]) is str + assert eval_typing(GetArg[t, A, 1]) == Never + + def test_uppercase_never(): d = eval_typing(Uppercase[Never]) assert d is Never diff --git a/typemap/type_eval/_eval_operators.py b/typemap/type_eval/_eval_operators.py index 7b74efe..cd313f3 100644 --- a/typemap/type_eval/_eval_operators.py +++ b/typemap/type_eval/_eval_operators.py @@ -1,6 +1,10 @@ +import collections +import collections.abc +import contextlib import functools import inspect import itertools +import re import types import typing @@ -22,6 +26,7 @@ Members, NewProtocol, Param, + SpecialFormEllipsis, StrConcat, StrSlice, Uncapitalize, @@ -266,15 +271,125 @@ def _get_args(tp, base, ctx) -> typing.Any: return None +def _fix_type(tp): + """Fix up a type getting returned from GetArg + + In particular, this means turning a list into a tuple of the list + elements and turning ... into SpecialFormEllipsis. + """ + if isinstance(tp, (tuple, list)): + return tuple[*tp] + elif tp is ...: + return SpecialFormEllipsis + else: + return tp + + +# The number of generic parameters to all the builtin types that had +# subscripting added in PEP 585. +_BUILTIN_GENERIC_ARITIES = { + tuple: 2, # variadic, like Callable... + list: 1, + dict: 2, + set: 1, + frozenset: 1, + type: 1, + collections.deque: 1, + collections.defaultdict: 2, + collections.OrderedDict: 2, + collections.Counter: 1, + collections.ChainMap: 2, + collections.abc.Awaitable: 1, + collections.abc.Coroutine: 3, + collections.abc.AsyncIterable: 1, + collections.abc.AsyncIterator: 1, + collections.abc.AsyncGenerator: 2, + collections.abc.Iterable: 1, + collections.abc.Iterator: 1, + collections.abc.Generator: 3, + collections.abc.Reversible: 1, + collections.abc.Container: 1, + collections.abc.Collection: 1, + collections.abc.Callable: 2, # special syntax + collections.abc.Set: 1, + collections.abc.MutableSet: 1, + collections.abc.Mapping: 2, + collections.abc.MutableMapping: 2, + collections.abc.Sequence: 1, + collections.abc.MutableSequence: 1, + collections.abc.KeysView: 1, + collections.abc.ItemsView: 2, + collections.abc.ValuesView: 1, + contextlib.AbstractContextManager: 1, + contextlib.AbstractAsyncContextManager: 1, + re.Pattern: 1, + re.Match: 1, +} + + +def _get_params(base_head): + if (params := getattr(base_head, "__parameters__", None)) is not None: + return params + elif (params := getattr(base_head, "__type_params__", None)) is not None: + return params + else: + return None + + +def _get_generic_arity(base_head): + if (n := _BUILTIN_GENERIC_ARITIES.get(base_head)) is not None: + return n + # XXX: check the type? + elif (n := getattr(base_head, "_nparams", None)) is not None: + return n + elif (params := _get_params(base_head)) is not None: + # TODO: also check for TypeVarTuple! + return len(params) + else: + return -1 + + +def _get_defaults(base_head): + """Get the *default* type params for a type + + `list` is equivalent to `list[Any]`, so `GetArg[list, list, 0] + ought to return `Any`, while `GetArg[list, list, 1]` ought to + return `Never` because the index is invalid. + + Annoyingly we need to consult a table for built-in arities for this. + """ + arity = _get_generic_arity(base_head) + if arity < 0: + return None + + # Callable and tuple need to produce a SpecialFormEllipsis for arg + # 0 and 1, respectively. + if base_head is collections.abc.Callable: + return (SpecialFormEllipsis, typing.Any) + elif base_head is tuple: + return (typing.Any, SpecialFormEllipsis) + + if params := _get_params(base_head): + return tuple( + typing.Any if t.__default__ == typing.NoDefault else t.__default__ + for t in params + ) + + return (typing.Any,) * arity + + @type_eval.register_evaluator(GetArg) @_lift_over_unions def _eval_GetArg(tp, base, idx, *, ctx) -> typing.Any: - args = _get_args(tp, base, ctx) + base_head = _typing_inspect.get_head(base) + args = _get_args(tp, base_head, ctx) + if args == (): + args = _get_defaults(base_head) if args is None: return typing.Never try: - return args[_from_literal(idx, ctx)] + return _fix_type(args[_from_literal(idx, ctx)]) except IndexError: return typing.Never diff --git a/typemap/typing.py b/typemap/typing.py index a9dff1d..56a1619 100644 --- a/typemap/typing.py +++ b/typemap/typing.py @@ -11,6 +11,10 @@ class BaseTypedDict(typing.TypedDict): pass +class SpecialFormEllipsis: + pass + + ###