From 38a9dd477d9a2077ac41c17bab1b87fc258629f2 Mon Sep 17 00:00:00 2001 From: "Michael J. Sullivan" Date: Thu, 9 Oct 2025 12:15:13 -0700 Subject: [PATCH 01/12] Add a couple tests --- tests/test_type_dir.py | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/tests/test_type_dir.py b/tests/test_type_dir.py index 98842a4..906a02e 100644 --- a/tests/test_type_dir.py +++ b/tests/test_type_dir.py @@ -61,6 +61,21 @@ class Final(Mine, Ordinary, Wrapper[float], AnotherBase[float], Last[int]): type OptionalFinal = AllOptional[Final] +type Capitalize[T] = next.NewProtocol[ + # Will .upper() be in our type evaluation language?? + [next.Property[p.name.upper(), p.type] for p in next.DirProperties[T]] +] + +type Prims[T] = next.NewProtocol[ + [ + next.Property[p.name, p.type] + for p in next.DirProperties[T] + if isinstance(p.type, type) + if issubclass(p.type, (int, str)) + ] +] + + def test_type_dir_1(): d = eval_typing(Final) @@ -92,3 +107,26 @@ class AllOptional[tests.test_type_dir.Final]: x: tests.test_type_dir.Wrapper[int | None] | None ordinary: str | None """) + + +def test_type_dir_3(): + d = eval_typing(Capitalize[Final]) + + assert format_helper.format_class(d) == textwrap.dedent("""\ + class Capitalize[tests.test_type_dir.Final]: + LAST: int | typing.Literal[True] + III: str | int | typing.Literal['gotcha!'] + T: dict[str, str | int | typing.Literal['gotcha!']] + KKK: ~K + X: tests.test_type_dir.Wrapper[int | None] + ORDINARY: str + """) + + +def test_type_dir_4(): + d = eval_typing(Prims[Final]) + + assert format_helper.format_class(d) == textwrap.dedent("""\ + class Prims[tests.test_type_dir.Final]: + ordinary: str + """) From 61fcafb844522954fce16146f47f6258f44d0fe4 Mon Sep 17 00:00:00 2001 From: "Michael J. Sullivan" Date: Thu, 9 Oct 2025 12:38:53 -0700 Subject: [PATCH 02/12] Add a IterUnion operation --- tests/test_type_dir.py | 32 ++++++++++++++++++++++++++++++++ typemap/typing.py | 15 +++++++++++++++ 2 files changed, 47 insertions(+) diff --git a/tests/test_type_dir.py b/tests/test_type_dir.py index 906a02e..6bbca67 100644 --- a/tests/test_type_dir.py +++ b/tests/test_type_dir.py @@ -76,6 +76,24 @@ class Final(Mine, Ordinary, Wrapper[float], AnotherBase[float], Last[int]): ] +type NoLiterals[T] = next.NewProtocol[ + [ + next.Property[ + p.name, + typing.Union[ + *[ + t + for t in next.IterUnion[p.type] + # XXX: Need a real way to check this?? + if not isinstance(t, typing._LiteralGenericAlias) + ] + ], + ] + for p in next.DirProperties[T] + ] +] + + def test_type_dir_1(): d = eval_typing(Final) @@ -130,3 +148,17 @@ def test_type_dir_4(): class Prims[tests.test_type_dir.Final]: ordinary: str """) + + +def test_type_dir_5(): + d = eval_typing(NoLiterals[Final]) + + assert format_helper.format_class(d) == textwrap.dedent("""\ + class NoLiterals[tests.test_type_dir.Final]: + last: int + iii: str | int + t: dict[str, str | int | typing.Literal['gotcha!']] + kkk: ~K + x: tests.test_type_dir.Wrapper[int | None] + ordinary: str + """) diff --git a/typemap/typing.py b/typemap/typing.py index 7de8c92..a1c881d 100644 --- a/typemap/typing.py +++ b/typemap/typing.py @@ -84,6 +84,21 @@ class DirProperties(metaclass=DirPropertiesMeta): ################################################################## +class IterUnionMeta(type): + def __getitem__(cls, tp): + if isinstance(tp, types.UnionType): + return tp.__args__ + else: + return [tp] + + +class IterUnion(metaclass=IterUnionMeta): + pass + + +################################################################## + + class NewProtocolMeta(type): def __getitem__(cls, val: list[Property]): dct: dict[str, object] = {} From 9b27dc74071a46af735c715bfe29c8fbfca379e2 Mon Sep 17 00:00:00 2001 From: "Michael J. Sullivan" Date: Fri, 10 Oct 2025 12:46:41 -0700 Subject: [PATCH 03/12] Add typing.GetAttr and some tests --- tests/test_call.py | 38 ++++++++++++++++++++++++++++++++++++++ typemap/typing.py | 14 ++++++++++++++ 2 files changed, 52 insertions(+) diff --git a/tests/test_call.py b/tests/test_call.py index ae4abc7..b79cbe2 100644 --- a/tests/test_call.py +++ b/tests/test_call.py @@ -23,3 +23,41 @@ class func[...]: b: int c: int """) + + +# Basic filtering +class Tgt: + pass + + +class A: + x: int + y: bool | None + z: Tgt + w: list[str] + + +def select[C: next.CallSpec]( + __rcv: A, *args: C.args, **kwargs: C.kwargs +) -> next.NewProtocol[ + [ + next.Property[ + c.name, + next.GetAttr[A, c.name], + ] + for c in next.CallSpecKwargs[C] + ] +]: ... + + +def test_call_2(): + ret = eval_call( + select, + A(), + x=True, + w=True, + ) + fmt = format_helper.format_class(ret) + + print() + print(fmt) diff --git a/typemap/typing.py b/typemap/typing.py index a1c881d..e48f67a 100644 --- a/typemap/typing.py +++ b/typemap/typing.py @@ -99,6 +99,20 @@ class IterUnion(metaclass=IterUnionMeta): ################################################################## +class GetAttrMeta(type): + def __getitem__(cls, arg): + lhs, prop = arg + # XXX: extras? + return typing.get_type_hints(lhs)[prop] + + +class GetAttr(metaclass=GetAttrMeta): + pass + + +################################################################## + + class NewProtocolMeta(type): def __getitem__(cls, val: list[Property]): dct: dict[str, object] = {} From bd6bbb348a836f39bb65b5a4e3f327d9f5c1c808 Mon Sep 17 00:00:00 2001 From: "Michael J. Sullivan" Date: Fri, 10 Oct 2025 17:26:12 -0700 Subject: [PATCH 04/12] Starting writing a test for a qblike thing --- tests/test_call.py | 38 ------------- tests/test_qblike.py | 128 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 128 insertions(+), 38 deletions(-) create mode 100644 tests/test_qblike.py diff --git a/tests/test_call.py b/tests/test_call.py index b79cbe2..ae4abc7 100644 --- a/tests/test_call.py +++ b/tests/test_call.py @@ -23,41 +23,3 @@ class func[...]: b: int c: int """) - - -# Basic filtering -class Tgt: - pass - - -class A: - x: int - y: bool | None - z: Tgt - w: list[str] - - -def select[C: next.CallSpec]( - __rcv: A, *args: C.args, **kwargs: C.kwargs -) -> next.NewProtocol[ - [ - next.Property[ - c.name, - next.GetAttr[A, c.name], - ] - for c in next.CallSpecKwargs[C] - ] -]: ... - - -def test_call_2(): - ret = eval_call( - select, - A(), - x=True, - w=True, - ) - fmt = format_helper.format_class(ret) - - print() - print(fmt) diff --git a/tests/test_qblike.py b/tests/test_qblike.py new file mode 100644 index 0000000..8844cc8 --- /dev/null +++ b/tests/test_qblike.py @@ -0,0 +1,128 @@ +import textwrap +import typing + +from typemap.type_eval import eval_call, eval_typing +from typemap import typing as next + +from . import format_helper + + +class Property[T]: + pass + + +class Link[T]: + pass + + +# XXX: We need to be able to check against _GenericAlias for our qb +# stuff, but this can't be how we do it. +def _is_alias_of(typ, cls): + return isinstance(typ, typing._GenericAlias) and issubclass( + typ.__origin__, cls + ) + + +type PropsOnly[T] = next.NewProtocol[ + [ + next.Property[p.name, p.type] + for p in next.DirProperties[T] + # Do we feel good about issubclass here? + # Argh! Stupid _GenericAlias crap! + # XXX: This is quite bad + if _is_alias_of(p.type, Property) + ] +] + +# XXX: How do we feel about a pure conditional type alias??? +# We could inline it if needed +type FilterLinks[T] = ( + # XXX: Arg access bad also... + # PropsOnly[T.__args__[0]] if _is_alias_of(T, Link) else T + # XXX: this fails -- attr error on __annotations__ in format_class + Link[PropsOnly[T.__args__[0]]] if _is_alias_of(T, Link) else T +) + + +# Basic filtering +class Tgt2: + pass + + +class Tgt: + name: Property[str] + tgt2: Link[Tgt2] + + +class A: + x: Property[int] + y: Property[bool | None] + z: Link[Tgt] + w: Property[list[str]] + + +def select[C: next.CallSpec]( + __rcv: A, *args: C.args, **kwargs: C.kwargs +) -> next.NewProtocol[ + [ + next.Property[ + c.name, + FilterLinks[next.GetAttr[A, c.name]], + ] + for c in next.CallSpecKwargs[C] + ] +]: ... + + +def test_qblike_1(): + ret = eval_call( + select, + A(), + x=True, + w=True, + ) + fmt = format_helper.format_class(ret) + + assert fmt == textwrap.dedent("""\ + class select[...]: + x: tests.test_qblike.Property[int] + w: tests.test_qblike.Property[list[str]] + """) + + +def test_qblike_2(): + ret = eval_typing(PropsOnly[A]) + fmt = format_helper.format_class(ret) + + assert fmt == textwrap.dedent("""\ + class PropsOnly[tests.test_qblike.A]: + x: tests.test_qblike.Property[int] + y: tests.test_qblike.Property[bool | None] + w: tests.test_qblike.Property[list[str]] + """) + + +def test_qblike_3(): + ret = eval_call( + select, + A(), + x=True, + w=True, + z=True, + ) + fmt = format_helper.format_class(ret) + + assert fmt == textwrap.dedent("""\ + class select[...]: + x: tests.test_qblike.Property[int] + w: tests.test_qblike.Property[list[str]] + z: tests.test_qblike.Link[PropsOnly[tests.test_qblike.Tgt]] + """) + + tgt = eval_typing(next.GetAttr[ret, "z"].__args__[0]) + fmt = format_helper.format_class(tgt) + + assert fmt == textwrap.dedent("""\ + class PropsOnly[tests.test_qblike.Tgt]: + name: tests.test_qblike.Property[str] + """) From 6ff62013b203fd9e411874454040c399aa99c164 Mon Sep 17 00:00:00 2001 From: "Michael J. Sullivan" Date: Tue, 14 Oct 2025 19:08:16 -0700 Subject: [PATCH 05/12] Make Property.name a Literal type This has the nice advantage of preventing us from doing expression computation on the name --- tests/test_qblike.py | 8 ++------ tests/test_type_dir.py | 11 ++++++++--- tests/test_type_eval.py | 15 ++++++++++++++- typemap/typing.py | 40 +++++++++++++++++++++++++++++++++++++--- 4 files changed, 61 insertions(+), 13 deletions(-) diff --git a/tests/test_qblike.py b/tests/test_qblike.py index 8844cc8..596b445 100644 --- a/tests/test_qblike.py +++ b/tests/test_qblike.py @@ -27,9 +27,7 @@ def _is_alias_of(typ, cls): [ next.Property[p.name, p.type] for p in next.DirProperties[T] - # Do we feel good about issubclass here? - # Argh! Stupid _GenericAlias crap! - # XXX: This is quite bad + # XXX: type language -- _is_alias_of if _is_alias_of(p.type, Property) ] ] @@ -37,9 +35,7 @@ def _is_alias_of(typ, cls): # XXX: How do we feel about a pure conditional type alias??? # We could inline it if needed type FilterLinks[T] = ( - # XXX: Arg access bad also... - # PropsOnly[T.__args__[0]] if _is_alias_of(T, Link) else T - # XXX: this fails -- attr error on __annotations__ in format_class + # XXX: type language -- _is_alias_of and __args__ Link[PropsOnly[T.__args__[0]]] if _is_alias_of(T, Link) else T ) diff --git a/tests/test_type_dir.py b/tests/test_type_dir.py index 6bbca67..04062fe 100644 --- a/tests/test_type_dir.py +++ b/tests/test_type_dir.py @@ -62,14 +62,17 @@ class Final(Mine, Ordinary, Wrapper[float], AnotherBase[float], Last[int]): type Capitalize[T] = next.NewProtocol[ - # Will .upper() be in our type evaluation language?? - [next.Property[p.name.upper(), p.type] for p in next.DirProperties[T]] + [ + next.Property[next.Uppercase[p.name], p.type] + for p in next.DirProperties[T] + ] ] type Prims[T] = next.NewProtocol[ [ next.Property[p.name, p.type] for p in next.DirProperties[T] + # XXX: type language -- check it better if isinstance(p.type, type) if issubclass(p.type, (int, str)) ] @@ -84,7 +87,9 @@ class Final(Mine, Ordinary, Wrapper[float], AnotherBase[float], Last[int]): *[ t for t in next.IterUnion[p.type] - # XXX: Need a real way to check this?? + # XXX: type language -- check it better + # maybe this one can't actually work well now? + # no way to do Literal[...]? if not isinstance(t, typing._LiteralGenericAlias) ] ], diff --git a/tests/test_type_eval.py b/tests/test_type_eval.py index 26b4a45..4f4fee9 100644 --- a/tests/test_type_eval.py +++ b/tests/test_type_eval.py @@ -1,5 +1,6 @@ import textwrap import typing +import unittest from typemap import typing as next from typemap.type_eval import eval_typing @@ -25,7 +26,7 @@ class F_int(F[int]): [ ( next.Property[p.name, OrGotcha[p.type]] - if p.type is not A + if p.type is not A # XXX -- hm! else next.Property[p.name, OrGotcha[MapRecursive[A]]] ) for p in (next.DirProperties[A] + next.DirProperties[F_int]) @@ -60,3 +61,15 @@ class MapRecursive[tests.test_type_eval.Recursive]: fff: int | typing.Literal['gotcha!'] control: float """) + + +# XXX: should this work??? +# probably not? +@unittest.skip +def test_eval_types_3(): + evaled = eval_typing(F[bool]) + + assert format_helper.format_class(evaled) == textwrap.dedent("""\ + class F[bool]: + fff: bool + """) diff --git a/typemap/typing.py b/typemap/typing.py index e48f67a..8550e6d 100644 --- a/typemap/typing.py +++ b/typemap/typing.py @@ -56,9 +56,17 @@ def CallSpecKwargs(self, spec: _CallSpecWrapper) -> list[_CallKwarg]: ################################################################## +def _from_literal(val): + if isinstance(val, typing._LiteralGenericAlias): # type: ignore[attr-defined] + val = val.__args__[0] + return val + + class PropertyMeta(type): - def __getitem__(cls, val: tuple[str, type]): - return cls(name=val[0], type=val[1]) + def __getitem__(cls, val: tuple[str | types.GenericAlias, type]): + name, type = val + # We allow str or Literal so that string literals work too + return cls(name=_from_literal(name), type=type) @dataclass(frozen=True) @@ -72,9 +80,10 @@ class Property(metaclass=PropertyMeta): class DirPropertiesMeta(type): def __getitem__(cls, tp): + # TODO: Support unions o = type_eval.eval_typing(tp) hints = typing.get_type_hints(o, include_extras=True) - return [Property(n, t) for n, t in hints.items()] + return [Property(typing.Literal[n], t) for n, t in hints.items()] class DirProperties(metaclass=DirPropertiesMeta): @@ -83,6 +92,11 @@ class DirProperties(metaclass=DirPropertiesMeta): ################################################################## +# IDEA: If we wanted to be more like typescript, we could make this +# the only acceptable argument to an `in` loop (and possibly rename it +# Iter?). We'd maybe drop DirProperties and use KeyOf or something +# instead... + class IterUnionMeta(type): def __getitem__(cls, tp): @@ -110,6 +124,26 @@ class GetAttr(metaclass=GetAttrMeta): pass +################################################################## + +# The type operators don't really need to be types... +# Maybe we should make all of them like this. + + +class _StringLiteralOp: + def __init__(self, op: typing.Callable[[str], str]): + self.op = op + + def __getitem__(self, arg): + return typing.Literal[self.op(_from_literal(arg))] + + +Uppercase = _StringLiteralOp(op=str.upper) +Lowercase = _StringLiteralOp(op=str.lower) +Capitalize = _StringLiteralOp(op=str.capitalize) +Uncapitalize = _StringLiteralOp(op=lambda s: s[0:1].lower() + s[1:]) + + ################################################################## From eab62fe51db0bc5be31e4d1103b91da50c277676 Mon Sep 17 00:00:00 2001 From: "Michael J. Sullivan" Date: Tue, 14 Oct 2025 19:21:07 -0700 Subject: [PATCH 06/12] Make DirProperties return be a tuple --- tests/test_type_dir.py | 8 ++++---- typemap/typing.py | 8 +++++++- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/tests/test_type_dir.py b/tests/test_type_dir.py index 04062fe..31cdfce 100644 --- a/tests/test_type_dir.py +++ b/tests/test_type_dir.py @@ -70,11 +70,11 @@ class Final(Mine, Ordinary, Wrapper[float], AnotherBase[float], Last[int]): type Prims[T] = next.NewProtocol[ [ - next.Property[p.name, p.type] - for p in next.DirProperties[T] + next.Property[name, typ] + for name, typ in next.DirProperties[T] # XXX: type language -- check it better - if isinstance(p.type, type) - if issubclass(p.type, (int, str)) + if isinstance(typ, type) + if issubclass(typ, (int, str)) ] ] diff --git a/typemap/typing.py b/typemap/typing.py index 8550e6d..f80bcb2 100644 --- a/typemap/typing.py +++ b/typemap/typing.py @@ -78,12 +78,18 @@ class Property(metaclass=PropertyMeta): ################################################################## +# I want to experiment with this being a tuple. +class _OutProperty(typing.NamedTuple): + name: str + type: type + + class DirPropertiesMeta(type): def __getitem__(cls, tp): # TODO: Support unions o = type_eval.eval_typing(tp) hints = typing.get_type_hints(o, include_extras=True) - return [Property(typing.Literal[n], t) for n, t in hints.items()] + return [_OutProperty(typing.Literal[n], t) for n, t in hints.items()] class DirProperties(metaclass=DirPropertiesMeta): From f8f058d3a852c42619bfc9113518a3fe6466ff69 Mon Sep 17 00:00:00 2001 From: "Michael J. Sullivan" Date: Tue, 14 Oct 2025 20:45:55 -0700 Subject: [PATCH 07/12] Add next.IsSubtype It dispatches to a new issubtype method. That is going to be the very hairy part of all this, I think. --- tests/test_qblike.py | 16 +--- tests/test_type_dir.py | 12 +-- tests/test_type_eval.py | 3 +- typemap/type_eval/__init__.py | 4 +- typemap/type_eval/_subtype.py | 85 +++++++++++++++++ typemap/type_eval/_typing_inspect.py | 136 +++++++++++++++++++++++++++ typemap/typing.py | 17 ++++ 7 files changed, 250 insertions(+), 23 deletions(-) create mode 100644 typemap/type_eval/_subtype.py create mode 100644 typemap/type_eval/_typing_inspect.py diff --git a/tests/test_qblike.py b/tests/test_qblike.py index 596b445..f68881e 100644 --- a/tests/test_qblike.py +++ b/tests/test_qblike.py @@ -1,5 +1,4 @@ import textwrap -import typing from typemap.type_eval import eval_call, eval_typing from typemap import typing as next @@ -15,28 +14,19 @@ class Link[T]: pass -# XXX: We need to be able to check against _GenericAlias for our qb -# stuff, but this can't be how we do it. -def _is_alias_of(typ, cls): - return isinstance(typ, typing._GenericAlias) and issubclass( - typ.__origin__, cls - ) - - type PropsOnly[T] = next.NewProtocol[ [ next.Property[p.name, p.type] for p in next.DirProperties[T] - # XXX: type language -- _is_alias_of - if _is_alias_of(p.type, Property) + if next.IsSubtype[p.type, Property] ] ] # XXX: How do we feel about a pure conditional type alias??? # We could inline it if needed type FilterLinks[T] = ( - # XXX: type language -- _is_alias_of and __args__ - Link[PropsOnly[T.__args__[0]]] if _is_alias_of(T, Link) else T + # XXX: type language -- __args__ + Link[PropsOnly[T.__args__[0]]] if next.IsSubtype[T, Link] else T ) diff --git a/tests/test_type_dir.py b/tests/test_type_dir.py index 31cdfce..04512de 100644 --- a/tests/test_type_dir.py +++ b/tests/test_type_dir.py @@ -72,9 +72,7 @@ class Final(Mine, Ordinary, Wrapper[float], AnotherBase[float], Last[int]): [ next.Property[name, typ] for name, typ in next.DirProperties[T] - # XXX: type language -- check it better - if isinstance(typ, type) - if issubclass(typ, (int, str)) + if next.IsSubtype[typ, int | str] ] ] @@ -87,10 +85,9 @@ class Final(Mine, Ordinary, Wrapper[float], AnotherBase[float], Last[int]): *[ t for t in next.IterUnion[p.type] - # XXX: type language -- check it better - # maybe this one can't actually work well now? - # no way to do Literal[...]? - if not isinstance(t, typing._LiteralGenericAlias) + # XXX: 'typing.Literal' is not *really* a type... + # Maybe we can't do this, which maybe is fine. + if not next.IsSubtype[t, typing.Literal] ] ], ] @@ -151,6 +148,7 @@ def test_type_dir_4(): assert format_helper.format_class(d) == textwrap.dedent("""\ class Prims[tests.test_type_dir.Final]: + last: int | typing.Literal[True] ordinary: str """) diff --git a/tests/test_type_eval.py b/tests/test_type_eval.py index 4f4fee9..13a76f1 100644 --- a/tests/test_type_eval.py +++ b/tests/test_type_eval.py @@ -26,9 +26,10 @@ class F_int(F[int]): [ ( next.Property[p.name, OrGotcha[p.type]] - if p.type is not A # XXX -- hm! + if not next.IsSubtype[p.type, A] else next.Property[p.name, OrGotcha[MapRecursive[A]]] ) + # XXX: type language - concatenating DirProperties is sketchy for p in (next.DirProperties[A] + next.DirProperties[F_int]) ] + [next.Property["control", float]] # noqa: F821 diff --git a/typemap/type_eval/__init__.py b/typemap/type_eval/__init__.py index 80690a4..fabbaee 100644 --- a/typemap/type_eval/__init__.py +++ b/typemap/type_eval/__init__.py @@ -1,6 +1,6 @@ from ._eval_call import eval_call from ._eval_typing import eval_typing, _get_current_context +from ._subtype import issubtype -__all__ = ("eval_typing", "eval_call", "_get_current_context") -1 +__all__ = ("eval_typing", "eval_call", "issubtype", "_get_current_context") diff --git a/typemap/type_eval/_subtype.py b/typemap/type_eval/_subtype.py new file mode 100644 index 0000000..f7ed7dc --- /dev/null +++ b/typemap/type_eval/_subtype.py @@ -0,0 +1,85 @@ +# import annotationlib + +# import contextlib +# import contextvars +# import dataclasses +# import functools +# import inspect +# import sys +# import types +import typing + + +# from . import _eval_type +from . import _typing_inspect + + +__all__ = ("issubtype",) + + +def issubtype(lhs: typing.Any, rhs: typing.Any) -> bool: + # TODO: Need to handle a lot of cases! + + # N.B: All of the 'bool's in these are because black otherwise + # formats the two-conditional chains in an unconscionably bad way. + + # Unions first + if _typing_inspect.is_union_type(rhs): + return any(issubtype(lhs, r) for r in typing.get_args(rhs)) + elif _typing_inspect.is_union_type(lhs): + return all(issubtype(t, rhs) for t in typing.get_args(lhs)) + + elif bool( + _typing_inspect.is_valid_isinstance_arg(lhs) + and _typing_inspect.is_valid_isinstance_arg(rhs) + ): + return issubclass(lhs, rhs) + + # literal <:? literal + elif bool( + _typing_inspect.is_literal(lhs) and _typing_inspect.is_literal(rhs) + ): + rhs_args = set(typing.get_args(rhs)) + return all(lv in rhs_args for lv in typing.get_args(lhs)) + + # XXX: This case is kind of a hack, to support NoLiterals. + elif rhs is typing.Literal: + return _typing_inspect.is_literal(lhs) + + # literal <:? type + elif _typing_inspect.is_literal(lhs): + return issubtype(type(typing.get_args(lhs)[0]), rhs) + + # C[A] <:? D + elif bool( + _typing_inspect.is_generic_alias(lhs) + # and _typing_inspect.is_valid_isinstance_arg(rhs) + ): + # print(lhs) + # breakpoint() + return issubclass(lhs.__origin__, rhs) + # return issubtype(lhs.__origin__, rhs) + # return issubtype(_typing_inspect.get_origin(lhs), rhs) + # C <:? D[A] + elif bool( + _typing_inspect.is_valid_isinstance_arg(lhs) + and _typing_inspect.is_generic_alias(rhs) + ): + return issubtype(lhs, _typing_inspect.get_origin(rhs)) + + # XXX: I think this is probably wrong, but a test currently has + # an unbound type variable... + elif _typing_inspect.is_type_var(lhs): + return lhs is rhs + + # TODO: What to do about C[A] <:? D[B]??? + + # TODO: Protocols??? + + # TODO: We will need to have some sort of hook to support runtime + # checking of typechecker extensions. + # + # We could have restrictions if we are willing to document them. + + # This will probably fail + return issubclass(lhs, rhs) diff --git a/typemap/type_eval/_typing_inspect.py b/typemap/type_eval/_typing_inspect.py new file mode 100644 index 0000000..66a60ed --- /dev/null +++ b/typemap/type_eval/_typing_inspect.py @@ -0,0 +1,136 @@ +# SPDX-PackageName: gel-python +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright Gel Data Inc. and the contributors. + + +import typing + +from typing import ( + Annotated, + Any, + ClassVar, + ForwardRef, + Literal, + TypeGuard, + TypeVar, + Union, + get_args, + get_origin, +) +from typing import _GenericAlias, _SpecialGenericAlias # type: ignore [attr-defined] # noqa: PLC2701 +from typing_extensions import TypeAliasType, TypeVarTuple, Unpack +from types import GenericAlias, UnionType + + +def is_classvar(t: Any) -> bool: + return t is ClassVar or (is_generic_alias(t) and get_origin(t) is ClassVar) # type: ignore [comparison-overlap] + + +def is_generic_alias(t: Any) -> TypeGuard[GenericAlias]: + return isinstance(t, (GenericAlias, _GenericAlias, _SpecialGenericAlias)) + + +def is_valid_type_arg(t: Any) -> bool: + return isinstance(t, type) or ( + is_generic_alias(t) and get_origin(t) is not Unpack # type: ignore [comparison-overlap] + ) + + +# In Python 3.10 isinstance(tuple[int], type) is True, but +# issubclass will fail if you pass such type to it. +def is_valid_isinstance_arg(t: Any) -> typing.TypeGuard[type[Any]]: + return isinstance(t, type) and not is_generic_alias(t) + + +def is_type_alias(t: Any) -> TypeGuard[TypeAliasType]: + return isinstance(t, TypeAliasType) and not is_generic_alias(t) + + +def is_type_var(t: Any) -> bool: + return type(t) is TypeVar + + +if (TypingTypeVarTuple := getattr(typing, "TypeVarTuple", None)) is not None: + + def is_type_var_tuple(t: Any) -> bool: + tt = type(t) + return tt is TypeVarTuple or tt is TypingTypeVarTuple + + def is_type_var_or_tuple(t: Any) -> bool: + tt = type(t) + return tt is TypeVar or tt is TypeVarTuple or tt is TypingTypeVarTuple +else: + + def is_type_var_tuple(t: Any) -> bool: + return type(t) is TypeVarTuple + + def is_type_var_or_tuple(t: Any) -> bool: + tt = type(t) + return tt is TypeVar or tt is TypeVarTuple + + +def is_type_var_tuple_unpack(t: Any) -> TypeGuard[GenericAlias]: + return ( + is_generic_alias(t) + and get_origin(t) is Unpack # type: ignore [comparison-overlap] + and is_type_var_tuple(get_args(t)[0]) + ) + + +def is_type_var_or_tuple_unpack(t: Any) -> bool: + return is_type_var(t) or is_type_var_tuple_unpack(t) + + +def is_generic_type_alias(t: Any) -> TypeGuard[GenericAlias]: + return is_generic_alias(t) and isinstance(get_origin(t), TypeAliasType) + + +def is_annotated(t: Any) -> TypeGuard[Annotated[Any, ...]]: + return is_generic_alias(t) and get_origin(t) is Annotated # type: ignore [comparison-overlap] + + +def is_forward_ref(t: Any) -> TypeGuard[ForwardRef]: + return isinstance(t, ForwardRef) + + +def contains_forward_refs(t: Any) -> bool: + if isinstance(t, (ForwardRef, str)): + # A direct ForwardRef or a PEP563/649 postponed annotation + return True + elif isinstance(t, TypeAliasType): + # PEP 695 type alias: unwrap and recurse + return contains_forward_refs(t.__value__) + elif args := get_args(t): + # Generic type: unwrap and recurse + return any(contains_forward_refs(arg) for arg in args) + else: + # No forward refs. + return False + + +def is_union_type(t: Any) -> TypeGuard[UnionType]: + return ( + (is_generic_alias(t) and get_origin(t) is Union) # type: ignore [comparison-overlap] + or isinstance(t, UnionType) + ) + + +def is_optional_type(t: Any) -> TypeGuard[UnionType]: + return is_union_type(t) and type(None) in get_args(t) + + +def is_literal(t: Any) -> bool: + return is_generic_alias(t) and get_origin(t) is Literal # type: ignore [comparison-overlap] + + +__all__ = ( + "is_annotated", + "is_classvar", + "is_forward_ref", + "is_generic_alias", + "is_generic_type_alias", + "is_literal", + "is_optional_type", + "is_type_alias", + "is_union_type", +) diff --git a/typemap/typing.py b/typemap/typing.py index f80bcb2..fe32bbf 100644 --- a/typemap/typing.py +++ b/typemap/typing.py @@ -121,6 +121,7 @@ class IterUnion(metaclass=IterUnionMeta): class GetAttrMeta(type): def __getitem__(cls, arg): + # TODO: Unions, the prop missing, etc! lhs, prop = arg # XXX: extras? return typing.get_type_hints(lhs)[prop] @@ -130,6 +131,22 @@ class GetAttr(metaclass=GetAttrMeta): pass +################################################################## + + +class IsSubtypeMeta(type): + def __getitem__(cls, arg): + lhs, rhs = arg + # return type_eval.issubtype( + # type_eval.eval_typing(lhs), type_eval.eval_typing(rhs) + # ) + return type_eval.issubtype(lhs, rhs) + + +class IsSubtype(metaclass=IsSubtypeMeta): + pass + + ################################################################## # The type operators don't really need to be types... From ecdbc87eb14837bed20a967d694917c4cf56e61f Mon Sep 17 00:00:00 2001 From: "Michael J. Sullivan" Date: Tue, 14 Oct 2025 21:23:38 -0700 Subject: [PATCH 08/12] Learn something new about __getitem__ --- tests/test_call.py | 2 +- tests/test_type_dir.py | 6 +++--- typemap/typing.py | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/test_call.py b/tests/test_call.py index ae4abc7..031ab6e 100644 --- a/tests/test_call.py +++ b/tests/test_call.py @@ -9,7 +9,7 @@ def func[C: next.CallSpec]( *args: C.args, **kwargs: C.kwargs ) -> next.NewProtocol[ - [next.Property[c.name, int] for c in next.CallSpecKwargs[C]] + *[next.Property[c.name, int] for c in next.CallSpecKwargs[C]] ]: ... diff --git a/tests/test_type_dir.py b/tests/test_type_dir.py index 04512de..9923bbc 100644 --- a/tests/test_type_dir.py +++ b/tests/test_type_dir.py @@ -55,7 +55,7 @@ class Final(Mine, Ordinary, Wrapper[float], AnotherBase[float], Last[int]): type AllOptional[T] = next.NewProtocol[ - [next.Property[p.name, p.type | None] for p in next.DirProperties[T]] + *[next.Property[p.name, p.type | None] for p in next.DirProperties[T]] ] type OptionalFinal = AllOptional[Final] @@ -69,7 +69,7 @@ class Final(Mine, Ordinary, Wrapper[float], AnotherBase[float], Last[int]): ] type Prims[T] = next.NewProtocol[ - [ + *[ next.Property[name, typ] for name, typ in next.DirProperties[T] if next.IsSubtype[typ, int | str] @@ -78,7 +78,7 @@ class Final(Mine, Ordinary, Wrapper[float], AnotherBase[float], Last[int]): type NoLiterals[T] = next.NewProtocol[ - [ + *[ next.Property[ p.name, typing.Union[ diff --git a/typemap/typing.py b/typemap/typing.py index fe32bbf..a2aaed0 100644 --- a/typemap/typing.py +++ b/typemap/typing.py @@ -171,7 +171,7 @@ def __getitem__(self, arg): class NewProtocolMeta(type): - def __getitem__(cls, val: list[Property]): + def __getitem__(cls, val: typing.Sequence[Property]): dct: dict[str, object] = {} dct["__annotations__"] = {prop.name: prop.type for prop in val} From 2817843fa6c9d36cb9247bd423fd4a6f68c73cbc Mon Sep 17 00:00:00 2001 From: "Michael J. Sullivan" Date: Tue, 14 Oct 2025 21:50:52 -0700 Subject: [PATCH 09/12] Switch to using _SpecialForm instead of all these metaclasses --- typemap/typing.py | 114 +++++++++++++++++++--------------------------- 1 file changed, 47 insertions(+), 67 deletions(-) diff --git a/typemap/typing.py b/typemap/typing.py index a2aaed0..3b3db7a 100644 --- a/typemap/typing.py +++ b/typemap/typing.py @@ -7,6 +7,9 @@ from typemap import type_eval +_SpecialForm: typing.Any = typing._SpecialForm + + @dataclass(frozen=True) class CallSpec: pass @@ -33,7 +36,7 @@ class _CallKwarg: name: str -@typing._SpecialForm # type: ignore[call-arg] +@_SpecialForm def CallSpecKwargs(self, spec: _CallSpecWrapper) -> list[_CallKwarg]: ff = types.FunctionType( spec._func.__code__, @@ -84,16 +87,12 @@ class _OutProperty(typing.NamedTuple): type: type -class DirPropertiesMeta(type): - def __getitem__(cls, tp): - # TODO: Support unions - o = type_eval.eval_typing(tp) - hints = typing.get_type_hints(o, include_extras=True) - return [_OutProperty(typing.Literal[n], t) for n, t in hints.items()] - - -class DirProperties(metaclass=DirPropertiesMeta): - pass +@_SpecialForm +def DirProperties(self, tp): + # TODO: Support unions + o = type_eval.eval_typing(tp) + hints = typing.get_type_hints(o, include_extras=True) + return [_OutProperty(typing.Literal[n], t) for n, t in hints.items()] ################################################################## @@ -104,54 +103,39 @@ class DirProperties(metaclass=DirPropertiesMeta): # instead... -class IterUnionMeta(type): - def __getitem__(cls, tp): - if isinstance(tp, types.UnionType): - return tp.__args__ - else: - return [tp] - - -class IterUnion(metaclass=IterUnionMeta): - pass +@_SpecialForm +def IterUnion(self, tp): + if isinstance(tp, types.UnionType): + return tp.__args__ + else: + return [tp] ################################################################## -class GetAttrMeta(type): - def __getitem__(cls, arg): - # TODO: Unions, the prop missing, etc! - lhs, prop = arg - # XXX: extras? - return typing.get_type_hints(lhs)[prop] - - -class GetAttr(metaclass=GetAttrMeta): - pass +@_SpecialForm +def GetAttr(self, arg): + # TODO: Unions, the prop missing, etc! + lhs, prop = arg + # XXX: extras? + return typing.get_type_hints(lhs)[prop] ################################################################## -class IsSubtypeMeta(type): - def __getitem__(cls, arg): - lhs, rhs = arg - # return type_eval.issubtype( - # type_eval.eval_typing(lhs), type_eval.eval_typing(rhs) - # ) - return type_eval.issubtype(lhs, rhs) - - -class IsSubtype(metaclass=IsSubtypeMeta): - pass +@_SpecialForm +def IsSubtype(self, arg): + lhs, rhs = arg + # return type_eval.issubtype( + # type_eval.eval_typing(lhs), type_eval.eval_typing(rhs) + # ) + return type_eval.issubtype(lhs, rhs) ################################################################## -# The type operators don't really need to be types... -# Maybe we should make all of them like this. - class _StringLiteralOp: def __init__(self, op: typing.Callable[[str], str]): @@ -170,29 +154,25 @@ def __getitem__(self, arg): ################################################################## -class NewProtocolMeta(type): - def __getitem__(cls, val: typing.Sequence[Property]): - dct: dict[str, object] = {} - dct["__annotations__"] = {prop.name: prop.type for prop in val} - - module_name = __name__ - name = "NewProtocol" - - # If the type evaluation context - ctx = type_eval._get_current_context() - if ctx.current_alias: - if isinstance(ctx.current_alias, types.GenericAlias): - name = str(ctx.current_alias) - else: - name = f"{ctx.current_alias.__name__}[...]" - module_name = ctx.current_alias.__module__ +@_SpecialForm +def NewProtocol(self, val: typing.Sequence[Property]): + dct: dict[str, object] = {} + dct["__annotations__"] = {prop.name: prop.type for prop in val} - dct["__module__"] = module_name + module_name = __name__ + name = "NewProtocol" - mcls: type = type(typing.cast(type, typing.Protocol)) - cls = mcls(name, (typing.Protocol,), dct) - return cls + # If the type evaluation context + ctx = type_eval._get_current_context() + if ctx.current_alias: + if isinstance(ctx.current_alias, types.GenericAlias): + name = str(ctx.current_alias) + else: + name = f"{ctx.current_alias.__name__}[...]" + module_name = ctx.current_alias.__module__ + dct["__module__"] = module_name -class NewProtocol(metaclass=NewProtocolMeta): - pass + mcls: type = type(typing.cast(type, typing.Protocol)) + cls = mcls(name, (typing.Protocol,), dct) + return cls From 9dccef46f720f8caaf7cea5b4d79c31e71c99976 Mon Sep 17 00:00:00 2001 From: "Michael J. Sullivan" Date: Tue, 14 Oct 2025 21:54:26 -0700 Subject: [PATCH 10/12] Add GetArg for extracting type arguments --- tests/test_qblike.py | 6 ++---- typemap/typing.py | 10 ++++++++++ 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/tests/test_qblike.py b/tests/test_qblike.py index f68881e..b0e7fae 100644 --- a/tests/test_qblike.py +++ b/tests/test_qblike.py @@ -22,11 +22,9 @@ class Link[T]: ] ] -# XXX: How do we feel about a pure conditional type alias??? -# We could inline it if needed +# Conditional type alias! type FilterLinks[T] = ( - # XXX: type language -- __args__ - Link[PropsOnly[T.__args__[0]]] if next.IsSubtype[T, Link] else T + Link[PropsOnly[next.GetArg[T, 0]]] if next.IsSubtype[T, Link] else T ) diff --git a/typemap/typing.py b/typemap/typing.py index 3b3db7a..8bb8744 100644 --- a/typemap/typing.py +++ b/typemap/typing.py @@ -122,6 +122,16 @@ def GetAttr(self, arg): return typing.get_type_hints(lhs)[prop] +@_SpecialForm +def GetArg(self, arg): + tp, idx = arg + args = typing.get_args(tp) + try: + return args[idx] + except IndexError: + return typing.Never + + ################################################################## From 9916d9733b24f6c6d03a6513527ef52d250faede Mon Sep 17 00:00:00 2001 From: "Michael J. Sullivan" Date: Tue, 14 Oct 2025 22:08:33 -0700 Subject: [PATCH 11/12] Start writing down a type language grammar More will be needed but this is basically what I've tested. --- spec-draft.rst | 63 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 spec-draft.rst diff --git a/spec-draft.rst b/spec-draft.rst new file mode 100644 index 0000000..26fb707 --- /dev/null +++ b/spec-draft.rst @@ -0,0 +1,63 @@ + +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. + + +:: + + = ... + | if else + + # Create NewProtocols and Unions using for loops. + # They can take either a single list comprehension as an + # argument, or starred list comprehensions can be included + # in the argument list. + + # TODO: NewProtocol needs a way of doing bases also... + # TODO: Should probably support Callable, TypedDict, etc + | NewProtocol[)>] + | NewProtocol[)> +] + + | Union[)>] + | Union[)> +] + + | GetAttr[, ] + | GetArg[, ] + + # String manipulation operations for string Literal types. + # We can put more in, but this is what typescript has. + | Uppercase[] | Lowercase[] + | Capitalize[] | Uncapitalize[] + + # Type conditional checks are just boolean compositions of + # subtype checking. + = + IsSubtype[, ] + | not + | and + | or + # Do we want these next two? + | any()>) + | all()>) + + = Property[, ] + + = + T , + | * , + + + = [ T + * ] + = + for in IterUnion + | for , in DirProperties + # TODO: callspecs + # TODO: variadic args (tuples, callables) + = + if + + + +``type-for(T)`` and ``variadic-type-arg(T)`` are parameterized grammar +rules, which can take different From 803e08e0315f818520a560f13608b234442d3567 Mon Sep 17 00:00:00 2001 From: "Michael J. Sullivan" Date: Tue, 14 Oct 2025 22:16:11 -0700 Subject: [PATCH 12/12] Make MapRecursive line up with type language a bit more --- tests/test_type_eval.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_type_eval.py b/tests/test_type_eval.py index 13a76f1..be32c2b 100644 --- a/tests/test_type_eval.py +++ b/tests/test_type_eval.py @@ -23,7 +23,7 @@ class F_int(F[int]): type MapRecursive[A] = next.NewProtocol[ - [ + *[ ( next.Property[p.name, OrGotcha[p.type]] if not next.IsSubtype[p.type, A] @@ -31,8 +31,8 @@ class F_int(F[int]): ) # XXX: type language - concatenating DirProperties is sketchy for p in (next.DirProperties[A] + next.DirProperties[F_int]) - ] - + [next.Property["control", float]] # noqa: F821 + ], + next.Property[typing.Literal["control"], float], ]