From 19c035fe916b8e4d013fec498816c518fb2f83c5 Mon Sep 17 00:00:00 2001 From: "Michael J. Sullivan" Date: Thu, 16 Oct 2025 13:06:47 -0700 Subject: [PATCH 01/19] Always require a * in the variadics --- spec-draft.rst | 12 ++++++++---- tests/test_qblike.py | 4 ++-- tests/test_type_dir.py | 2 +- typemap/typing.py | 5 ++++- 4 files changed, 15 insertions(+), 8 deletions(-) diff --git a/spec-draft.rst b/spec-draft.rst index 26fb707..2569fae 100644 --- a/spec-draft.rst +++ b/spec-draft.rst @@ -3,6 +3,12 @@ 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: +- Drop DirProperties - make it Members or something +- IsSubtype -> Is? +- Look into TupleTypeVar stuff for iteration +- Move some to a "primitives" section + :: @@ -16,10 +22,8 @@ It's important that there be a clearly specified type language for the type-leve # TODO: NewProtocol needs a way of doing bases also... # TODO: Should probably support Callable, TypedDict, etc - | NewProtocol[)>] | NewProtocol[)> +] - | Union[)>] | Union[)> +] | GetAttr[, ] @@ -38,8 +42,8 @@ It's important that there be a clearly specified type language for the type-leve | and | or # Do we want these next two? - | any()>) - | all()>) + | Any[)>] + | All[)>] = Property[, ] diff --git a/tests/test_qblike.py b/tests/test_qblike.py index b0e7fae..67c3ba7 100644 --- a/tests/test_qblike.py +++ b/tests/test_qblike.py @@ -15,7 +15,7 @@ class Link[T]: type PropsOnly[T] = next.NewProtocol[ - [ + *[ next.Property[p.name, p.type] for p in next.DirProperties[T] if next.IsSubtype[p.type, Property] @@ -48,7 +48,7 @@ class A: 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]], diff --git a/tests/test_type_dir.py b/tests/test_type_dir.py index 9923bbc..09ebff6 100644 --- a/tests/test_type_dir.py +++ b/tests/test_type_dir.py @@ -62,7 +62,7 @@ class Final(Mine, Ordinary, Wrapper[float], AnotherBase[float], Last[int]): type Capitalize[T] = next.NewProtocol[ - [ + *[ next.Property[next.Uppercase[p.name], p.type] for p in next.DirProperties[T] ] diff --git a/typemap/typing.py b/typemap/typing.py index 8bb8744..a7234a0 100644 --- a/typemap/typing.py +++ b/typemap/typing.py @@ -165,7 +165,10 @@ def __getitem__(self, arg): @_SpecialForm -def NewProtocol(self, val: typing.Sequence[Property]): +def NewProtocol(self, val: Property | tuple[Property, ...]): + if not isinstance(val, tuple): + val = (val,) + dct: dict[str, object] = {} dct["__annotations__"] = {prop.name: prop.type for prop in val} From e855fa26eaa260e20b11912b0757b229caf14580 Mon Sep 17 00:00:00 2001 From: "Michael J. Sullivan" Date: Thu, 16 Oct 2025 13:57:41 -0700 Subject: [PATCH 02/19] Spec update - move lots to library --- spec-draft.rst | 59 ++++++++++++++++++++++++++++++++------------------ 1 file changed, 38 insertions(+), 21 deletions(-) diff --git a/spec-draft.rst b/spec-draft.rst index 2569fae..567db1d 100644 --- a/spec-draft.rst +++ b/spec-draft.rst @@ -9,30 +9,22 @@ TODO: - Look into TupleTypeVar stuff for iteration - Move some to a "primitives" section +Big Q: what should be an error and what should return Never? + :: = ... | 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[)> +] - - | Union[)> +] + # Types with variadic arguments can have + # *[... for t in ...] arguments + | [)> +] - | GetAttr[, ] + # This is syntax because taking an int literal makes it a + # special form. | 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. @@ -54,14 +46,39 @@ TODO: = [ T + * ] = - for in IterUnion - | for , in DirProperties - # TODO: callspecs - # TODO: variadic args (tuples, callables) + # Iterate over a tuple type + for in Iter = if +``type-for(T)`` is a parameterized grammar rule, which can take +different types. Not sure if we actually want this though. + +--- + +# TODO: NewProtocol needs a way of doing bases also... +# TODO: New TypedDict setup +* ``NewProtocol[*Ps: Property]`` + +* ``Member[N: Literal & str, T]`` +# These names are too long -- but we can't do ``Type`` !! +* ``GetName[T: Member]`` +* ``GetType[T: Member]`` + +--- + +* ``GetAttr[T, S: Literal & str]`` + +# TODO: how to deal with special forms like Callable and tuple[T, ...] +* ``GetArgs[T]`` - returns a tuple containing all of the type arguments + + + +String manipulation operations for string Literal types. +We can put more in, but this is what typescript has. -``type-for(T)`` and ``variadic-type-arg(T)`` are parameterized grammar -rules, which can take different +* ``Uppercase[S: Literal & str]`` +* ``Lowercase[S: Literal & str]`` +* ``Capitalize[S: Literal & str]`` +* ``Uncapitalize[S: Literal & str]`` From c4905d60db54179583658c4fb8961a841a5f4f52 Mon Sep 17 00:00:00 2001 From: "Michael J. Sullivan" Date: Thu, 16 Oct 2025 14:02:11 -0700 Subject: [PATCH 03/19] Start changing things to use GetName/GetType --- tests/test_call.py | 2 +- tests/test_qblike.py | 8 ++++---- tests/test_type_dir.py | 17 ++++++++++------- tests/test_type_eval.py | 6 +++--- typemap/typing.py | 30 ++++++++++++++++++------------ 5 files changed, 36 insertions(+), 27 deletions(-) diff --git a/tests/test_call.py b/tests/test_call.py index 031ab6e..31bc64f 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[next.GetName[c], int] for c in next.CallSpecKwargs[C]] ]: ... diff --git a/tests/test_qblike.py b/tests/test_qblike.py index 67c3ba7..d79bec6 100644 --- a/tests/test_qblike.py +++ b/tests/test_qblike.py @@ -16,9 +16,9 @@ class Link[T]: type PropsOnly[T] = next.NewProtocol[ *[ - next.Property[p.name, p.type] + p for p in next.DirProperties[T] - if next.IsSubtype[p.type, Property] + if next.IsSubtype[next.GetType[p], Property] ] ] @@ -50,8 +50,8 @@ def select[C: next.CallSpec]( ) -> next.NewProtocol[ *[ next.Property[ - c.name, - FilterLinks[next.GetAttr[A, c.name]], + next.GetName[c], + FilterLinks[next.GetAttr[A, next.GetName[c]]], ] for c in next.CallSpecKwargs[C] ] diff --git a/tests/test_type_dir.py b/tests/test_type_dir.py index 09ebff6..a9e162f 100644 --- a/tests/test_type_dir.py +++ b/tests/test_type_dir.py @@ -55,7 +55,10 @@ 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[next.GetName[p], next.GetType[p] | None] + for p in next.DirProperties[T] + ] ] type OptionalFinal = AllOptional[Final] @@ -63,16 +66,16 @@ class Final(Mine, Ordinary, Wrapper[float], AnotherBase[float], Last[int]): type Capitalize[T] = next.NewProtocol[ *[ - next.Property[next.Uppercase[p.name], p.type] + next.Property[next.Uppercase[next.GetName[p]], next.GetType[p]] for p in next.DirProperties[T] ] ] type Prims[T] = next.NewProtocol[ *[ - next.Property[name, typ] - for name, typ in next.DirProperties[T] - if next.IsSubtype[typ, int | str] + p + for p in next.DirProperties[T] + if next.IsSubtype[next.GetType[p], int | str] ] ] @@ -80,11 +83,11 @@ class Final(Mine, Ordinary, Wrapper[float], AnotherBase[float], Last[int]): type NoLiterals[T] = next.NewProtocol[ *[ next.Property[ - p.name, + next.GetName[p], typing.Union[ *[ t - for t in next.IterUnion[p.type] + for t in next.IterUnion[next.GetType[p]] # 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] diff --git a/tests/test_type_eval.py b/tests/test_type_eval.py index be32c2b..e0f9c68 100644 --- a/tests/test_type_eval.py +++ b/tests/test_type_eval.py @@ -25,9 +25,9 @@ class F_int(F[int]): type MapRecursive[A] = next.NewProtocol[ *[ ( - next.Property[p.name, OrGotcha[p.type]] - if not next.IsSubtype[p.type, A] - else next.Property[p.name, OrGotcha[MapRecursive[A]]] + next.Property[next.GetName[p], OrGotcha[next.GetType[p]]] + if not next.IsSubtype[next.GetType[p], A] + else next.Property[next.GetName[p], OrGotcha[MapRecursive[A]]] ) # XXX: type language - concatenating DirProperties is sketchy for p in (next.DirProperties[A] + next.DirProperties[F_int]) diff --git a/typemap/typing.py b/typemap/typing.py index a7234a0..47bb11b 100644 --- a/typemap/typing.py +++ b/typemap/typing.py @@ -33,7 +33,7 @@ def kwargs(self) -> None: @dataclass(frozen=True) class _CallKwarg: - name: str + _name: str @_SpecialForm @@ -53,7 +53,7 @@ def CallSpecKwargs(self, spec: _CallSpecWrapper) -> list[_CallKwarg]: sig = inspect.signature(ff) bound = sig.bind(*spec._args, **spec._kwargs) - return [_CallKwarg(name=name) for name in bound.kwargs] + return [_CallKwarg(_name=name) for name in bound.kwargs] ################################################################## @@ -69,22 +69,26 @@ class PropertyMeta(type): 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) + return cls(_name=_from_literal(name), _type=type) @dataclass(frozen=True) class Property(metaclass=PropertyMeta): - name: str - type: type + _name: str + _type: type -################################################################## +@_SpecialForm +def GetName(self, tp): + return tp._name + + +@_SpecialForm +def GetType(self, tp): + return tp._type -# I want to experiment with this being a tuple. -class _OutProperty(typing.NamedTuple): - name: str - type: type +################################################################## @_SpecialForm @@ -92,7 +96,7 @@ 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()] + return [Property(typing.Literal[n], t) for n, t in hints.items()] ################################################################## @@ -170,7 +174,9 @@ def NewProtocol(self, val: Property | tuple[Property, ...]): val = (val,) dct: dict[str, object] = {} - dct["__annotations__"] = {prop.name: prop.type for prop in val} + dct["__annotations__"] = { + _from_literal(GetName[prop]): GetType[prop] for prop in val + } module_name = __name__ name = "NewProtocol" From 5766e5da9ed6155a4cc660742a4d7ef1465eed77 Mon Sep 17 00:00:00 2001 From: "Michael J. Sullivan" Date: Thu, 16 Oct 2025 14:11:01 -0700 Subject: [PATCH 04/19] Rename DirProperties -> Attrs --- tests/test_qblike.py | 6 +----- tests/test_type_dir.py | 16 ++++++---------- tests/test_type_eval.py | 2 +- typemap/typing.py | 2 +- 4 files changed, 9 insertions(+), 17 deletions(-) diff --git a/tests/test_qblike.py b/tests/test_qblike.py index d79bec6..5606fce 100644 --- a/tests/test_qblike.py +++ b/tests/test_qblike.py @@ -15,11 +15,7 @@ class Link[T]: type PropsOnly[T] = next.NewProtocol[ - *[ - p - for p in next.DirProperties[T] - if next.IsSubtype[next.GetType[p], Property] - ] + *[p for p in next.Attrs[T] if next.IsSubtype[next.GetType[p], Property]] ] # Conditional type alias! diff --git a/tests/test_type_dir.py b/tests/test_type_dir.py index a9e162f..b52c7a9 100644 --- a/tests/test_type_dir.py +++ b/tests/test_type_dir.py @@ -57,7 +57,7 @@ class Final(Mine, Ordinary, Wrapper[float], AnotherBase[float], Last[int]): type AllOptional[T] = next.NewProtocol[ *[ next.Property[next.GetName[p], next.GetType[p] | None] - for p in next.DirProperties[T] + for p in next.Attrs[T] ] ] @@ -67,16 +67,12 @@ class Final(Mine, Ordinary, Wrapper[float], AnotherBase[float], Last[int]): type Capitalize[T] = next.NewProtocol[ *[ next.Property[next.Uppercase[next.GetName[p]], next.GetType[p]] - for p in next.DirProperties[T] + for p in next.Attrs[T] ] ] type Prims[T] = next.NewProtocol[ - *[ - p - for p in next.DirProperties[T] - if next.IsSubtype[next.GetType[p], int | str] - ] + *[p for p in next.Attrs[T] if next.IsSubtype[next.GetType[p], int | str]] ] @@ -94,7 +90,7 @@ class Final(Mine, Ordinary, Wrapper[float], AnotherBase[float], Last[int]): ] ], ] - for p in next.DirProperties[T] + for p in next.Attrs[T] ] ] @@ -119,8 +115,8 @@ def sbase[Z](cls, a: int | Literal['gotcha!'] | Z | None, b: ~K) -> dict[str, in def test_type_dir_2(): d = eval_typing(OptionalFinal) - # XXX: `DirProperties` skips methods, true to its name. Perhaps we just need - # `Dir` that would iterate over everything + # XXX: `Atrs` skips methods, true to its name. Perhaps we just need + # `Members` that would iterate over everything assert format_helper.format_class(d) == textwrap.dedent("""\ class AllOptional[tests.test_type_dir.Final]: last: int | typing.Literal[True] | None diff --git a/tests/test_type_eval.py b/tests/test_type_eval.py index e0f9c68..c771f74 100644 --- a/tests/test_type_eval.py +++ b/tests/test_type_eval.py @@ -30,7 +30,7 @@ class F_int(F[int]): else next.Property[next.GetName[p], OrGotcha[MapRecursive[A]]] ) # XXX: type language - concatenating DirProperties is sketchy - for p in (next.DirProperties[A] + next.DirProperties[F_int]) + for p in (next.Attrs[A] + next.Attrs[F_int]) ], next.Property[typing.Literal["control"], float], ] diff --git a/typemap/typing.py b/typemap/typing.py index 47bb11b..73441a8 100644 --- a/typemap/typing.py +++ b/typemap/typing.py @@ -92,7 +92,7 @@ def GetType(self, tp): @_SpecialForm -def DirProperties(self, tp): +def Attrs(self, tp): # TODO: Support unions o = type_eval.eval_typing(tp) hints = typing.get_type_hints(o, include_extras=True) From df4a6dc7f1c8c2be9a9f351a1df6ec5f6c7df243 Mon Sep 17 00:00:00 2001 From: "Michael J. Sullivan" Date: Thu, 16 Oct 2025 14:13:05 -0700 Subject: [PATCH 05/19] Rename Property -> Member --- tests/test_call.py | 2 +- tests/test_qblike.py | 2 +- tests/test_type_dir.py | 6 +++--- tests/test_type_eval.py | 6 +++--- typemap/typing.py | 8 ++++---- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/tests/test_call.py b/tests/test_call.py index 31bc64f..8f60062 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[next.GetName[c], int] for c in next.CallSpecKwargs[C]] + *[next.Member[next.GetName[c], int] for c in next.CallSpecKwargs[C]] ]: ... diff --git a/tests/test_qblike.py b/tests/test_qblike.py index 5606fce..8dad6d1 100644 --- a/tests/test_qblike.py +++ b/tests/test_qblike.py @@ -45,7 +45,7 @@ def select[C: next.CallSpec]( __rcv: A, *args: C.args, **kwargs: C.kwargs ) -> next.NewProtocol[ *[ - next.Property[ + next.Member[ next.GetName[c], FilterLinks[next.GetAttr[A, next.GetName[c]]], ] diff --git a/tests/test_type_dir.py b/tests/test_type_dir.py index b52c7a9..9115f85 100644 --- a/tests/test_type_dir.py +++ b/tests/test_type_dir.py @@ -56,7 +56,7 @@ class Final(Mine, Ordinary, Wrapper[float], AnotherBase[float], Last[int]): type AllOptional[T] = next.NewProtocol[ *[ - next.Property[next.GetName[p], next.GetType[p] | None] + next.Member[next.GetName[p], next.GetType[p] | None] for p in next.Attrs[T] ] ] @@ -66,7 +66,7 @@ class Final(Mine, Ordinary, Wrapper[float], AnotherBase[float], Last[int]): type Capitalize[T] = next.NewProtocol[ *[ - next.Property[next.Uppercase[next.GetName[p]], next.GetType[p]] + next.Member[next.Uppercase[next.GetName[p]], next.GetType[p]] for p in next.Attrs[T] ] ] @@ -78,7 +78,7 @@ class Final(Mine, Ordinary, Wrapper[float], AnotherBase[float], Last[int]): type NoLiterals[T] = next.NewProtocol[ *[ - next.Property[ + next.Member[ next.GetName[p], typing.Union[ *[ diff --git a/tests/test_type_eval.py b/tests/test_type_eval.py index c771f74..d916f14 100644 --- a/tests/test_type_eval.py +++ b/tests/test_type_eval.py @@ -25,14 +25,14 @@ class F_int(F[int]): type MapRecursive[A] = next.NewProtocol[ *[ ( - next.Property[next.GetName[p], OrGotcha[next.GetType[p]]] + next.Member[next.GetName[p], OrGotcha[next.GetType[p]]] if not next.IsSubtype[next.GetType[p], A] - else next.Property[next.GetName[p], OrGotcha[MapRecursive[A]]] + else next.Member[next.GetName[p], OrGotcha[MapRecursive[A]]] ) # XXX: type language - concatenating DirProperties is sketchy for p in (next.Attrs[A] + next.Attrs[F_int]) ], - next.Property[typing.Literal["control"], float], + next.Member[typing.Literal["control"], float], ] diff --git a/typemap/typing.py b/typemap/typing.py index 73441a8..a21ca17 100644 --- a/typemap/typing.py +++ b/typemap/typing.py @@ -65,7 +65,7 @@ def _from_literal(val): return val -class PropertyMeta(type): +class MemberMeta(type): def __getitem__(cls, val: tuple[str | types.GenericAlias, type]): name, type = val # We allow str or Literal so that string literals work too @@ -73,7 +73,7 @@ def __getitem__(cls, val: tuple[str | types.GenericAlias, type]): @dataclass(frozen=True) -class Property(metaclass=PropertyMeta): +class Member(metaclass=MemberMeta): _name: str _type: type @@ -96,7 +96,7 @@ def Attrs(self, 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 [Member(typing.Literal[n], t) for n, t in hints.items()] ################################################################## @@ -169,7 +169,7 @@ def __getitem__(self, arg): @_SpecialForm -def NewProtocol(self, val: Property | tuple[Property, ...]): +def NewProtocol(self, val: Member | tuple[Member, ...]): if not isinstance(val, tuple): val = (val,) From 7c31285090c7899d7dfb1c5b9a8c23c0c3936811 Mon Sep 17 00:00:00 2001 From: "Michael J. Sullivan" Date: Thu, 16 Oct 2025 15:01:46 -0700 Subject: [PATCH 06/19] Make Property, GetName, and GetType into derived forms This requires doing a lot more eval_typing in the implementation than before. --- typemap/typing.py | 70 ++++++++++++++++++++++++----------------------- 1 file changed, 36 insertions(+), 34 deletions(-) diff --git a/typemap/typing.py b/typemap/typing.py index a21ca17..42962fe 100644 --- a/typemap/typing.py +++ b/typemap/typing.py @@ -31,13 +31,8 @@ def kwargs(self) -> None: pass -@dataclass(frozen=True) -class _CallKwarg: - _name: str - - @_SpecialForm -def CallSpecKwargs(self, spec: _CallSpecWrapper) -> list[_CallKwarg]: +def CallSpecKwargs(self, spec: _CallSpecWrapper) -> list[type[Member]]: ff = types.FunctionType( spec._func.__code__, spec._func.__globals__, @@ -53,39 +48,32 @@ def CallSpecKwargs(self, spec: _CallSpecWrapper) -> list[_CallKwarg]: sig = inspect.signature(ff) bound = sig.bind(*spec._args, **spec._kwargs) - return [_CallKwarg(_name=name) for name in bound.kwargs] + # TODO: Get the real type instead of Never + return [ + Member[ + typing.Literal[name], # type: ignore[valid-type] + typing.Never, + ] + for name in bound.kwargs + ] ################################################################## def _from_literal(val): + val = type_eval.eval_typing(val) if isinstance(val, typing._LiteralGenericAlias): # type: ignore[attr-defined] val = val.__args__[0] return val -class MemberMeta(type): - 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) -class Member(metaclass=MemberMeta): - _name: str - _type: type - - -@_SpecialForm -def GetName(self, tp): - return tp._name +class Member[N: str, T]: + pass -@_SpecialForm -def GetType(self, tp): - return tp._type +type GetName[T: Member] = GetArg[T, 0] # type: ignore[valid-type] +type GetType[T: Member] = GetArg[T, 1] # type: ignore[valid-type] ################################################################## @@ -96,7 +84,7 @@ def Attrs(self, tp): # TODO: Support unions o = type_eval.eval_typing(tp) hints = typing.get_type_hints(o, include_extras=True) - return [Member(typing.Literal[n], t) for n, t in hints.items()] + return [Member[typing.Literal[n], t] for n, t in hints.items()] ################################################################## @@ -109,6 +97,7 @@ def Attrs(self, tp): @_SpecialForm def IterUnion(self, tp): + tp = type_eval.eval_typing(tp) if isinstance(tp, types.UnionType): return tp.__args__ else: @@ -123,13 +112,14 @@ def GetAttr(self, arg): # TODO: Unions, the prop missing, etc! lhs, prop = arg # XXX: extras? - return typing.get_type_hints(lhs)[prop] + name = _from_literal(type_eval.eval_typing(prop)) + return typing.get_type_hints(type_eval.eval_typing(lhs))[name] @_SpecialForm def GetArg(self, arg): tp, idx = arg - args = typing.get_args(tp) + args = typing.get_args(type_eval.eval_typing(tp)) try: return args[idx] except IndexError: @@ -142,10 +132,14 @@ def GetArg(self, arg): @_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) + return type_eval.issubtype( + type_eval.eval_typing(lhs), + # XXX: This is solidly wrong, we need to eval both sides... + # But eval_typing currently expands generic types out into + # something broken... + # type_eval.eval_typing(rhs), + rhs, + ) ################################################################## @@ -173,9 +167,17 @@ def NewProtocol(self, val: Member | tuple[Member, ...]): if not isinstance(val, tuple): val = (val,) + etyps = [type_eval.eval_typing(t) for t in val] + dct: dict[str, object] = {} dct["__annotations__"] = { - _from_literal(GetName[prop]): GetType[prop] for prop in val + # XXX: Should eval_typing on the etyps evaluate the arguments?? + _from_literal(type_eval.eval_typing(typing.get_args(prop)[0])): + # XXX: We maybe (probably?) want to eval_typing the RHS, but + # we have infinite recursion issues in test_eval_types_2... + # type_eval.eval_typing(typing.get_args(prop)[1]) + typing.get_args(prop)[1] + for prop in etyps } module_name = __name__ From be7492097575fee27f84422126c337fca746b0ab Mon Sep 17 00:00:00 2001 From: "Michael J. Sullivan" Date: Thu, 16 Oct 2025 16:02:21 -0700 Subject: [PATCH 07/19] Try a custom implementation of IsLiteral - kind of broken --- tests/test_type_dir.py | 57 +++++++++++++++++++++++++++++++++++++++--- typemap/typing.py | 11 +++++++- 2 files changed, 63 insertions(+), 5 deletions(-) diff --git a/tests/test_type_dir.py b/tests/test_type_dir.py index 9115f85..762170e 100644 --- a/tests/test_type_dir.py +++ b/tests/test_type_dir.py @@ -75,8 +75,7 @@ class Final(Mine, Ordinary, Wrapper[float], AnotherBase[float], Last[int]): *[p for p in next.Attrs[T] if next.IsSubtype[next.GetType[p], int | str]] ] - -type NoLiterals[T] = next.NewProtocol[ +type NoLiterals1[T] = next.NewProtocol[ *[ next.Member[ next.GetName[p], @@ -95,6 +94,41 @@ class Final(Mine, Ordinary, Wrapper[float], AnotherBase[float], Last[int]): ] +# Try to implement IsLiteral. This is basically what is recommended +# for doing it in TS. +# XXX: This doesn't work in python! We can subtype str! +type IsLiteral[T] = ( + typing.Literal[True] + if ( + (next.IsSubtype[T, str] and not next.IsSubtype[str, T]) + or (next.IsSubtype[T, bytes] and not next.IsSubtype[bytes, T]) + or (next.IsSubtype[T, bool] and not next.IsSubtype[bool, T]) + or (next.IsSubtype[T, int] and not next.IsSubtype[int, T]) + # XXX: enum, None + ) + else typing.Literal[False] +) + +type NoLiterals2[T] = next.NewProtocol[ + *[ + next.Member[ + next.GetName[p], + typing.Union[ + *[ + t + for t in next.IterUnion[next.GetType[p]] + # 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] + if not next.IsSubtype[IsLiteral[t], typing.Literal[True]] + ] + ], + ] + for p in next.Attrs[T] + ] +] + + def test_type_dir_1(): d = eval_typing(Final) @@ -153,10 +187,25 @@ class Prims[tests.test_type_dir.Final]: def test_type_dir_5(): - d = eval_typing(NoLiterals[Final]) + global fuck + d = eval_typing(NoLiterals1[Final]) + + assert format_helper.format_class(d) == textwrap.dedent("""\ + class NoLiterals1[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 + """) + + +def test_type_dir_6(): + d = eval_typing(NoLiterals2[Final]) assert format_helper.format_class(d) == textwrap.dedent("""\ - class NoLiterals[tests.test_type_dir.Final]: + class NoLiterals2[tests.test_type_dir.Final]: last: int iii: str | int t: dict[str, str | int | typing.Literal['gotcha!']] diff --git a/typemap/typing.py b/typemap/typing.py index 42962fe..2d6a656 100644 --- a/typemap/typing.py +++ b/typemap/typing.py @@ -10,6 +10,11 @@ _SpecialForm: typing.Any = typing._SpecialForm +class _NoCacheSpecialForm(_SpecialForm, _root=True): # type: ignore[call-arg] + def __getitem__(self, parameters): + return self._getitem(self, parameters) + + @dataclass(frozen=True) class CallSpec: pass @@ -162,7 +167,11 @@ def __getitem__(self, arg): ################################################################## -@_SpecialForm +# XXX: We definitely can't use the normal _SpecialForm cache here +# directly, since we depend on the context's current_alias. +# Maybe we can add that to the cache, though. +# (Or maybe we need to never use the cache??) +@_NoCacheSpecialForm def NewProtocol(self, val: Member | tuple[Member, ...]): if not isinstance(val, tuple): val = (val,) From fbd4748aa94ce325f2d2b0359fdb7e7ddeaa4f6f Mon Sep 17 00:00:00 2001 From: "Michael J. Sullivan" Date: Thu, 16 Oct 2025 16:08:48 -0700 Subject: [PATCH 08/19] Rename IsSubtype to Is --- spec-draft.rst | 9 ++++----- tests/test_qblike.py | 4 ++-- tests/test_type_dir.py | 14 +++++++------- tests/test_type_eval.py | 2 +- typemap/typing.py | 2 +- 5 files changed, 15 insertions(+), 16 deletions(-) diff --git a/spec-draft.rst b/spec-draft.rst index 567db1d..0bc30d0 100644 --- a/spec-draft.rst +++ b/spec-draft.rst @@ -29,16 +29,15 @@ Big Q: what should be an error and what should return Never? # Type conditional checks are just boolean compositions of # subtype checking. = - IsSubtype[, ] + Is[, ] | not | and | or - # Do we want these next two? + + # Do we want these next two? Probably not. | Any[)>] | All[)>] - = Property[, ] - = T , | * , @@ -59,7 +58,7 @@ different types. Not sure if we actually want this though. # TODO: NewProtocol needs a way of doing bases also... # TODO: New TypedDict setup -* ``NewProtocol[*Ps: Property]`` +* ``NewProtocol[*Ps: Member]`` * ``Member[N: Literal & str, T]`` # These names are too long -- but we can't do ``Type`` !! diff --git a/tests/test_qblike.py b/tests/test_qblike.py index 8dad6d1..c8774a1 100644 --- a/tests/test_qblike.py +++ b/tests/test_qblike.py @@ -15,12 +15,12 @@ class Link[T]: type PropsOnly[T] = next.NewProtocol[ - *[p for p in next.Attrs[T] if next.IsSubtype[next.GetType[p], Property]] + *[p for p in next.Attrs[T] if next.Is[next.GetType[p], Property]] ] # Conditional type alias! type FilterLinks[T] = ( - Link[PropsOnly[next.GetArg[T, 0]]] if next.IsSubtype[T, Link] else T + Link[PropsOnly[next.GetArg[T, 0]]] if next.Is[T, Link] else T ) diff --git a/tests/test_type_dir.py b/tests/test_type_dir.py index 762170e..28f5899 100644 --- a/tests/test_type_dir.py +++ b/tests/test_type_dir.py @@ -72,7 +72,7 @@ class Final(Mine, Ordinary, Wrapper[float], AnotherBase[float], Last[int]): ] type Prims[T] = next.NewProtocol[ - *[p for p in next.Attrs[T] if next.IsSubtype[next.GetType[p], int | str]] + *[p for p in next.Attrs[T] if next.Is[next.GetType[p], int | str]] ] type NoLiterals1[T] = next.NewProtocol[ @@ -85,7 +85,7 @@ class Final(Mine, Ordinary, Wrapper[float], AnotherBase[float], Last[int]): for t in next.IterUnion[next.GetType[p]] # 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] + if not next.Is[t, typing.Literal] ] ], ] @@ -100,10 +100,10 @@ class Final(Mine, Ordinary, Wrapper[float], AnotherBase[float], Last[int]): type IsLiteral[T] = ( typing.Literal[True] if ( - (next.IsSubtype[T, str] and not next.IsSubtype[str, T]) - or (next.IsSubtype[T, bytes] and not next.IsSubtype[bytes, T]) - or (next.IsSubtype[T, bool] and not next.IsSubtype[bool, T]) - or (next.IsSubtype[T, int] and not next.IsSubtype[int, T]) + (next.Is[T, str] and not next.Is[str, T]) + or (next.Is[T, bytes] and not next.Is[bytes, T]) + or (next.Is[T, bool] and not next.Is[bool, T]) + or (next.Is[T, int] and not next.Is[int, T]) # XXX: enum, None ) else typing.Literal[False] @@ -120,7 +120,7 @@ class Final(Mine, Ordinary, Wrapper[float], AnotherBase[float], Last[int]): # 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] - if not next.IsSubtype[IsLiteral[t], typing.Literal[True]] + if not next.Is[IsLiteral[t], typing.Literal[True]] ] ], ] diff --git a/tests/test_type_eval.py b/tests/test_type_eval.py index d916f14..61a729e 100644 --- a/tests/test_type_eval.py +++ b/tests/test_type_eval.py @@ -26,7 +26,7 @@ class F_int(F[int]): *[ ( next.Member[next.GetName[p], OrGotcha[next.GetType[p]]] - if not next.IsSubtype[next.GetType[p], A] + if not next.Is[next.GetType[p], A] else next.Member[next.GetName[p], OrGotcha[MapRecursive[A]]] ) # XXX: type language - concatenating DirProperties is sketchy diff --git a/typemap/typing.py b/typemap/typing.py index 2d6a656..896c8ed 100644 --- a/typemap/typing.py +++ b/typemap/typing.py @@ -135,7 +135,7 @@ def GetArg(self, arg): @_SpecialForm -def IsSubtype(self, arg): +def Is(self, arg): lhs, rhs = arg return type_eval.issubtype( type_eval.eval_typing(lhs), From 20b29a2f3c953ae50d2973d9e8000cea06c7296c Mon Sep 17 00:00:00 2001 From: "Michael J. Sullivan" Date: Thu, 16 Oct 2025 16:28:47 -0700 Subject: [PATCH 09/19] Make Iter over tuples *the* iteration construct --- spec-draft.rst | 5 ++++- tests/test_call.py | 5 ++++- tests/test_qblike.py | 4 ++-- tests/test_type_dir.py | 18 +++++++++++------- tests/test_type_eval.py | 11 +++++++++-- typemap/typing.py | 41 ++++++++++++++++++++++++++++++----------- 6 files changed, 60 insertions(+), 24 deletions(-) diff --git a/spec-draft.rst b/spec-draft.rst index 0bc30d0..9e36592 100644 --- a/spec-draft.rst +++ b/spec-draft.rst @@ -46,7 +46,7 @@ Big Q: what should be an error and what should return Never? = [ T + * ] = # Iterate over a tuple type - for in Iter + for in Iter[] = if @@ -71,7 +71,10 @@ different types. Not sure if we actually want this though. # TODO: how to deal with special forms like Callable and tuple[T, ...] * ``GetArgs[T]`` - returns a tuple containing all of the type arguments +* ``FromUnion[T]`` - returns a tuple containing all of the union + elements, or a 1-ary tuple containing T if it is not a union. +# TODO: How to do IsUnion? String manipulation operations for string Literal types. diff --git a/tests/test_call.py b/tests/test_call.py index 8f60062..f8384d8 100644 --- a/tests/test_call.py +++ b/tests/test_call.py @@ -9,7 +9,10 @@ def func[C: next.CallSpec]( *args: C.args, **kwargs: C.kwargs ) -> next.NewProtocol[ - *[next.Member[next.GetName[c], int] for c in next.CallSpecKwargs[C]] + *[ + next.Member[next.GetName[c], int] + for c in next.Iter[next.CallSpecKwargs[C]] + ] ]: ... diff --git a/tests/test_qblike.py b/tests/test_qblike.py index c8774a1..f1f9f08 100644 --- a/tests/test_qblike.py +++ b/tests/test_qblike.py @@ -15,7 +15,7 @@ class Link[T]: type PropsOnly[T] = next.NewProtocol[ - *[p for p in next.Attrs[T] if next.Is[next.GetType[p], Property]] + *[p for p in next.Iter[next.Attrs[T]] if next.Is[next.GetType[p], Property]] ] # Conditional type alias! @@ -49,7 +49,7 @@ def select[C: next.CallSpec]( next.GetName[c], FilterLinks[next.GetAttr[A, next.GetName[c]]], ] - for c in next.CallSpecKwargs[C] + for c in next.Iter[next.CallSpecKwargs[C]] ] ]: ... diff --git a/tests/test_type_dir.py b/tests/test_type_dir.py index 28f5899..aadfcad 100644 --- a/tests/test_type_dir.py +++ b/tests/test_type_dir.py @@ -57,7 +57,7 @@ class Final(Mine, Ordinary, Wrapper[float], AnotherBase[float], Last[int]): type AllOptional[T] = next.NewProtocol[ *[ next.Member[next.GetName[p], next.GetType[p] | None] - for p in next.Attrs[T] + for p in next.Iter[next.Attrs[T]] ] ] @@ -67,12 +67,16 @@ class Final(Mine, Ordinary, Wrapper[float], AnotherBase[float], Last[int]): type Capitalize[T] = next.NewProtocol[ *[ next.Member[next.Uppercase[next.GetName[p]], next.GetType[p]] - for p in next.Attrs[T] + for p in next.Iter[next.Attrs[T]] ] ] type Prims[T] = next.NewProtocol[ - *[p for p in next.Attrs[T] if next.Is[next.GetType[p], int | str]] + *[ + p + for p in next.Iter[next.Attrs[T]] + if next.Is[next.GetType[p], int | str] + ] ] type NoLiterals1[T] = next.NewProtocol[ @@ -82,14 +86,14 @@ class Final(Mine, Ordinary, Wrapper[float], AnotherBase[float], Last[int]): typing.Union[ *[ t - for t in next.IterUnion[next.GetType[p]] + for t in next.Iter[next.FromUnion[next.GetType[p]]] # XXX: 'typing.Literal' is not *really* a type... # Maybe we can't do this, which maybe is fine. if not next.Is[t, typing.Literal] ] ], ] - for p in next.Attrs[T] + for p in next.Iter[next.Attrs[T]] ] ] @@ -116,7 +120,7 @@ class Final(Mine, Ordinary, Wrapper[float], AnotherBase[float], Last[int]): typing.Union[ *[ t - for t in next.IterUnion[next.GetType[p]] + for t in next.Iter[next.FromUnion[next.GetType[p]]] # 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] @@ -124,7 +128,7 @@ class Final(Mine, Ordinary, Wrapper[float], AnotherBase[float], Last[int]): ] ], ] - for p in next.Attrs[T] + for p in next.Iter[next.Attrs[T]] ] ] diff --git a/tests/test_type_eval.py b/tests/test_type_eval.py index 61a729e..eb11739 100644 --- a/tests/test_type_eval.py +++ b/tests/test_type_eval.py @@ -22,6 +22,11 @@ class F_int(F[int]): pass +type ConcatTuples[A, B] = tuple[ + *[x for x in next.Iter[A]], + *[x for x in next.Iter[B]], +] + type MapRecursive[A] = next.NewProtocol[ *[ ( @@ -29,8 +34,10 @@ class F_int(F[int]): if not next.Is[next.GetType[p], A] else next.Member[next.GetName[p], OrGotcha[MapRecursive[A]]] ) - # XXX: type language - concatenating DirProperties is sketchy - for p in (next.Attrs[A] + next.Attrs[F_int]) + # XXX: This next line *ought* to work, but we haven't + # implemented it yet. + # for p in next.Iter[*next.Attrs[A], *next.Attrs[F_int]] + for p in next.Iter[ConcatTuples[next.Attrs[A], next.Attrs[F_int]]] ], next.Member[typing.Literal["control"], float], ] diff --git a/typemap/typing.py b/typemap/typing.py index 896c8ed..70b70fe 100644 --- a/typemap/typing.py +++ b/typemap/typing.py @@ -5,6 +5,7 @@ import typing from typemap import type_eval +from typemap.type_eval import _typing_inspect _SpecialForm: typing.Any = typing._SpecialForm @@ -37,7 +38,7 @@ def kwargs(self) -> None: @_SpecialForm -def CallSpecKwargs(self, spec: _CallSpecWrapper) -> list[type[Member]]: +def CallSpecKwargs(self, spec: _CallSpecWrapper): ff = types.FunctionType( spec._func.__code__, spec._func.__globals__, @@ -54,12 +55,14 @@ def CallSpecKwargs(self, spec: _CallSpecWrapper) -> list[type[Member]]: bound = sig.bind(*spec._args, **spec._kwargs) # TODO: Get the real type instead of Never - return [ - Member[ - typing.Literal[name], # type: ignore[valid-type] - typing.Never, + return tuple[ + *[ # type: ignore[misc] + Member[ + typing.Literal[name], # type: ignore[valid-type] + typing.Never, + ] + for name in bound.kwargs ] - for name in bound.kwargs ] @@ -68,7 +71,7 @@ def CallSpecKwargs(self, spec: _CallSpecWrapper) -> list[type[Member]]: def _from_literal(val): val = type_eval.eval_typing(val) - if isinstance(val, typing._LiteralGenericAlias): # type: ignore[attr-defined] + if _typing_inspect.is_literal(val): val = val.__args__[0] return val @@ -89,7 +92,7 @@ def Attrs(self, tp): # TODO: Support unions o = type_eval.eval_typing(tp) hints = typing.get_type_hints(o, include_extras=True) - return [Member[typing.Literal[n], t] for n, t in hints.items()] + return tuple[*[Member[typing.Literal[n], t] for n, t in hints.items()]] ################################################################## @@ -101,12 +104,28 @@ def Attrs(self, tp): @_SpecialForm -def IterUnion(self, tp): +def Iter(self, tp): tp = type_eval.eval_typing(tp) - if isinstance(tp, types.UnionType): + if ( + _typing_inspect.is_generic_alias(tp) + and tp.__origin__ is tuple + and (not tp.__args__ or tp.__args__[-1] is not Ellipsis) + ): return tp.__args__ else: - return [tp] + # XXX: Or should we return []? + raise TypeError( + f"Invalid type argument to Iter: {tp} is not a fixed-length tuple" + ) + + +@_SpecialForm +def FromUnion(self, tp): + tp = type_eval.eval_typing(tp) + if isinstance(tp, types.UnionType): + return tuple[*tp.__args__] + else: + return tuple[tp] ################################################################## From 8d7b87e93a1c84078dc863d95a7021d73bf32506 Mon Sep 17 00:00:00 2001 From: "Michael J. Sullivan" Date: Thu, 16 Oct 2025 16:48:48 -0700 Subject: [PATCH 10/19] Spec tweak --- spec-draft.rst | 3 --- typemap/typing.py | 6 ------ 2 files changed, 9 deletions(-) diff --git a/spec-draft.rst b/spec-draft.rst index 9e36592..906aa57 100644 --- a/spec-draft.rst +++ b/spec-draft.rst @@ -4,10 +4,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: -- Drop DirProperties - make it Members or something -- IsSubtype -> Is? - Look into TupleTypeVar stuff for iteration -- Move some to a "primitives" section Big Q: what should be an error and what should return Never? diff --git a/typemap/typing.py b/typemap/typing.py index 70b70fe..23ef85f 100644 --- a/typemap/typing.py +++ b/typemap/typing.py @@ -97,12 +97,6 @@ def Attrs(self, tp): ################################################################## -# 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... - - @_SpecialForm def Iter(self, tp): tp = type_eval.eval_typing(tp) From 6460aa9aeb058666f74d30ba0b3c09be791c1ba8 Mon Sep 17 00:00:00 2001 From: "Michael J. Sullivan" Date: Thu, 16 Oct 2025 17:34:02 -0700 Subject: [PATCH 11/19] Import all the names from typing and next --- spec-draft.rst | 3 +- tests/test_call.py | 18 +++++---- tests/test_qblike.py | 36 +++++++++++------- tests/test_type_dir.py | 84 ++++++++++++++++++++--------------------- tests/test_type_eval.py | 38 +++++++++++-------- typemap/typing.py | 5 ++- 6 files changed, 104 insertions(+), 80 deletions(-) diff --git a/spec-draft.rst b/spec-draft.rst index 906aa57..8082c58 100644 --- a/spec-draft.rst +++ b/spec-draft.rst @@ -49,7 +49,8 @@ Big Q: what should be an error and what should return Never? ``type-for(T)`` is a parameterized grammar rule, which can take -different types. Not sure if we actually want this though. +different types. Not sure if we actually need this though---now it is +only used for Any/All. --- diff --git a/tests/test_call.py b/tests/test_call.py index f8384d8..2138404 100644 --- a/tests/test_call.py +++ b/tests/test_call.py @@ -1,18 +1,22 @@ import textwrap from typemap.type_eval import eval_call -from typemap import typing as next +from typemap.typing import ( + CallSpec, + NewProtocol, + Member, + GetName, + Iter, + CallSpecKwargs, +) from . import format_helper -def func[C: next.CallSpec]( +def func[C: CallSpec]( *args: C.args, **kwargs: C.kwargs -) -> next.NewProtocol[ - *[ - next.Member[next.GetName[c], int] - for c in next.Iter[next.CallSpecKwargs[C]] - ] +) -> NewProtocol[ + *[Member[GetName[c], int] for c in Iter[CallSpecKwargs[C]]] ]: ... diff --git a/tests/test_qblike.py b/tests/test_qblike.py index f1f9f08..ac8833f 100644 --- a/tests/test_qblike.py +++ b/tests/test_qblike.py @@ -1,7 +1,19 @@ import textwrap from typemap.type_eval import eval_call, eval_typing -from typemap import typing as next +from typemap.typing import ( + NewProtocol, + Iter, + Attrs, + Is, + GetType, + CallSpec, + Member, + GetName, + GetAttr, + CallSpecKwargs, + GetArg, +) from . import format_helper @@ -14,14 +26,12 @@ class Link[T]: pass -type PropsOnly[T] = next.NewProtocol[ - *[p for p in next.Iter[next.Attrs[T]] if next.Is[next.GetType[p], Property]] +type PropsOnly[T] = NewProtocol[ + *[p for p in Iter[Attrs[T]] if Is[GetType[p], Property]] ] # Conditional type alias! -type FilterLinks[T] = ( - Link[PropsOnly[next.GetArg[T, 0]]] if next.Is[T, Link] else T -) +type FilterLinks[T] = Link[PropsOnly[GetArg[T, 0]]] if Is[T, Link] else T # Basic filtering @@ -41,15 +51,15 @@ class A: w: Property[list[str]] -def select[C: next.CallSpec]( +def select[C: CallSpec]( __rcv: A, *args: C.args, **kwargs: C.kwargs -) -> next.NewProtocol[ +) -> NewProtocol[ *[ - next.Member[ - next.GetName[c], - FilterLinks[next.GetAttr[A, next.GetName[c]]], + Member[ + GetName[c], + FilterLinks[GetAttr[A, GetName[c]]], ] - for c in next.Iter[next.CallSpecKwargs[C]] + for c in Iter[CallSpecKwargs[C]] ] ]: ... @@ -99,7 +109,7 @@ class select[...]: z: tests.test_qblike.Link[PropsOnly[tests.test_qblike.Tgt]] """) - tgt = eval_typing(next.GetAttr[ret, "z"].__args__[0]) + tgt = eval_typing(GetAttr[ret, "z"].__args__[0]) fmt = format_helper.format_class(tgt) assert fmt == textwrap.dedent("""\ diff --git a/tests/test_type_dir.py b/tests/test_type_dir.py index aadfcad..7bc1d99 100644 --- a/tests/test_type_dir.py +++ b/tests/test_type_dir.py @@ -1,13 +1,23 @@ -import typing import textwrap +from typing import TypeVar, Literal, Union from typemap.type_eval import eval_typing -from typemap import typing as next +from typemap.typing import ( + NewProtocol, + Member, + GetName, + GetType, + Iter, + Attrs, + FromUnion, + Uppercase, + Is, +) from . import format_helper -type OrGotcha[K] = K | typing.Literal["gotcha!"] +type OrGotcha[K] = K | Literal["gotcha!"] type StrForInt[X] = (str | OrGotcha[X]) if X is int else (X | OrGotcha[X]) @@ -21,7 +31,7 @@ class AnotherBase[I]: class Base[T]: - K = typing.TypeVar("K") + K = TypeVar("K") t: dict[str, StrForInt[T]] kkk: K @@ -47,53 +57,43 @@ class Mine(Wrapper[int]): class Last[O]: - last: O | typing.Literal[True] + last: O | Literal[True] class Final(Mine, Ordinary, Wrapper[float], AnotherBase[float], Last[int]): pass -type AllOptional[T] = next.NewProtocol[ - *[ - next.Member[next.GetName[p], next.GetType[p] | None] - for p in next.Iter[next.Attrs[T]] - ] +type AllOptional[T] = NewProtocol[ + *[Member[GetName[p], GetType[p] | None] for p in Iter[Attrs[T]]] ] type OptionalFinal = AllOptional[Final] -type Capitalize[T] = next.NewProtocol[ - *[ - next.Member[next.Uppercase[next.GetName[p]], next.GetType[p]] - for p in next.Iter[next.Attrs[T]] - ] +type Capitalize[T] = NewProtocol[ + *[Member[Uppercase[GetName[p]], GetType[p]] for p in Iter[Attrs[T]]] ] -type Prims[T] = next.NewProtocol[ - *[ - p - for p in next.Iter[next.Attrs[T]] - if next.Is[next.GetType[p], int | str] - ] +type Prims[T] = NewProtocol[ + *[p for p in Iter[Attrs[T]] if Is[GetType[p], int | str]] ] -type NoLiterals1[T] = next.NewProtocol[ +type NoLiterals1[T] = NewProtocol[ *[ - next.Member[ - next.GetName[p], - typing.Union[ + Member[ + GetName[p], + Union[ *[ t - for t in next.Iter[next.FromUnion[next.GetType[p]]] + for t in Iter[FromUnion[GetType[p]]] # XXX: 'typing.Literal' is not *really* a type... # Maybe we can't do this, which maybe is fine. - if not next.Is[t, typing.Literal] + if not Is[t, Literal] ] ], ] - for p in next.Iter[next.Attrs[T]] + for p in Iter[Attrs[T]] ] ] @@ -102,33 +102,33 @@ class Final(Mine, Ordinary, Wrapper[float], AnotherBase[float], Last[int]): # for doing it in TS. # XXX: This doesn't work in python! We can subtype str! type IsLiteral[T] = ( - typing.Literal[True] + Literal[True] if ( - (next.Is[T, str] and not next.Is[str, T]) - or (next.Is[T, bytes] and not next.Is[bytes, T]) - or (next.Is[T, bool] and not next.Is[bool, T]) - or (next.Is[T, int] and not next.Is[int, T]) + (Is[T, str] and not Is[str, T]) + or (Is[T, bytes] and not Is[bytes, T]) + or (Is[T, bool] and not Is[bool, T]) + or (Is[T, int] and not Is[int, T]) # XXX: enum, None ) - else typing.Literal[False] + else Literal[False] ) -type NoLiterals2[T] = next.NewProtocol[ +type NoLiterals2[T] = NewProtocol[ *[ - next.Member[ - next.GetName[p], - typing.Union[ + Member[ + GetName[p], + Union[ *[ t - for t in next.Iter[next.FromUnion[next.GetType[p]]] + for t in Iter[FromUnion[GetType[p]]] # 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] - if not next.Is[IsLiteral[t], typing.Literal[True]] + # if not IsSubtype[t, Literal] + if not Is[IsLiteral[t], Literal[True]] ] ], ] - for p in next.Iter[next.Attrs[T]] + for p in Iter[Attrs[T]] ] ] diff --git a/tests/test_type_eval.py b/tests/test_type_eval.py index eb11739..1cc5506 100644 --- a/tests/test_type_eval.py +++ b/tests/test_type_eval.py @@ -1,17 +1,25 @@ import textwrap -import typing import unittest - -from typemap import typing as next +from typing import Literal + +from typemap.typing import ( + NewProtocol, + Member, + GetName, + GetType, + Iter, + Attrs, + Is, +) from typemap.type_eval import eval_typing from . import format_helper -type A[T] = T | None | typing.Literal[False] +type A[T] = T | None | Literal[False] type B = A[int] -type OrGotcha[K] = K | typing.Literal["gotcha!"] +type OrGotcha[K] = K | Literal["gotcha!"] class F[T]: @@ -23,30 +31,30 @@ class F_int(F[int]): type ConcatTuples[A, B] = tuple[ - *[x for x in next.Iter[A]], - *[x for x in next.Iter[B]], + *[x for x in Iter[A]], + *[x for x in Iter[B]], ] -type MapRecursive[A] = next.NewProtocol[ +type MapRecursive[A] = NewProtocol[ *[ ( - next.Member[next.GetName[p], OrGotcha[next.GetType[p]]] - if not next.Is[next.GetType[p], A] - else next.Member[next.GetName[p], OrGotcha[MapRecursive[A]]] + Member[GetName[p], OrGotcha[GetType[p]]] + if not Is[GetType[p], A] + else Member[GetName[p], OrGotcha[MapRecursive[A]]] ) # XXX: This next line *ought* to work, but we haven't # implemented it yet. - # for p in next.Iter[*next.Attrs[A], *next.Attrs[F_int]] - for p in next.Iter[ConcatTuples[next.Attrs[A], next.Attrs[F_int]]] + # for p in Iter[*Attrs[A], *Attrs[F_int]] + for p in Iter[ConcatTuples[Attrs[A], Attrs[F_int]]] ], - next.Member[typing.Literal["control"], float], + Member[Literal["control"], float], ] class Recursive: n: int m: str - t: typing.Literal[False] + t: Literal[False] a: Recursive diff --git a/typemap/typing.py b/typemap/typing.py index 23ef85f..5bcfe0f 100644 --- a/typemap/typing.py +++ b/typemap/typing.py @@ -55,8 +55,8 @@ def CallSpecKwargs(self, spec: _CallSpecWrapper): bound = sig.bind(*spec._args, **spec._kwargs) # TODO: Get the real type instead of Never - return tuple[ - *[ # type: ignore[misc] + return tuple[ # type: ignore[misc] + *[ Member[ typing.Literal[name], # type: ignore[valid-type] typing.Never, @@ -97,6 +97,7 @@ def Attrs(self, tp): ################################################################## + @_SpecialForm def Iter(self, tp): tp = type_eval.eval_typing(tp) From 19ec76932d6b90cd6472a0de04b6f0b594ff180d Mon Sep 17 00:00:00 2001 From: "Michael J. Sullivan" Date: Fri, 17 Oct 2025 16:20:44 -0700 Subject: [PATCH 12/19] Dael with eval_type results better in issubtype --- typemap/type_eval/__init__.py | 10 ++++++++-- typemap/type_eval/_eval_typing.py | 10 +++++++++- typemap/type_eval/_subtype.py | 27 +++++++++++---------------- typemap/type_eval/_typing_inspect.py | 6 ++++++ typemap/typing.py | 6 +----- 5 files changed, 35 insertions(+), 24 deletions(-) diff --git a/typemap/type_eval/__init__.py b/typemap/type_eval/__init__.py index fabbaee..9ce08fa 100644 --- a/typemap/type_eval/__init__.py +++ b/typemap/type_eval/__init__.py @@ -1,6 +1,12 @@ from ._eval_call import eval_call -from ._eval_typing import eval_typing, _get_current_context +from ._eval_typing import eval_typing, _get_current_context, _EvalProxy from ._subtype import issubtype -__all__ = ("eval_typing", "eval_call", "issubtype", "_get_current_context") +__all__ = ( + "eval_typing", + "eval_call", + "issubtype", + "_EvalProxy", + "_get_current_context", +) diff --git a/typemap/type_eval/_eval_typing.py b/typemap/type_eval/_eval_typing.py index 29f1f37..e68ebaa 100644 --- a/typemap/type_eval/_eval_typing.py +++ b/typemap/type_eval/_eval_typing.py @@ -19,6 +19,13 @@ __all__ = ("eval_typing",) +# Base type for the proxy classes we generate to hold __annotations__ +class _EvalProxy: + # Make sure __origin__ doesn't show up at runtime... + if typing.TYPE_CHECKING: + __origin__: type + + @dataclasses.dataclass class EvalContext: seen: dict[Any, Any] @@ -101,10 +108,11 @@ def _eval_type_type(obj: type, ctx: EvalContext): if isinstance(obj, type) and issubclass(obj, typing.Generic): ret = type( obj.__name__, - (typing.cast(type, typing.Protocol),), + (_EvalProxy,), { "__module__": obj.__module__, "__name__": obj.__name__, + "__origin__": obj, }, ) diff --git a/typemap/type_eval/_subtype.py b/typemap/type_eval/_subtype.py index f7ed7dc..1366d6c 100644 --- a/typemap/type_eval/_subtype.py +++ b/typemap/type_eval/_subtype.py @@ -1,16 +1,6 @@ -# 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 @@ -29,6 +19,14 @@ def issubtype(lhs: typing.Any, rhs: typing.Any) -> bool: elif _typing_inspect.is_union_type(lhs): return all(issubtype(t, rhs) for t in typing.get_args(lhs)) + # For _EvalProxy's just blow through them, since we don't yet care + # about the attribute types here. + # TODO: But we'll need to once we support Protocols?? + elif _typing_inspect.is_eval_proxy(lhs): + return issubtype(lhs.__origin__, rhs) + elif _typing_inspect.is_eval_proxy(rhs): + return issubtype(lhs, rhs.__origin__) + elif bool( _typing_inspect.is_valid_isinstance_arg(lhs) and _typing_inspect.is_valid_isinstance_arg(rhs) @@ -53,13 +51,10 @@ def issubtype(lhs: typing.Any, rhs: typing.Any) -> bool: # C[A] <:? D elif bool( _typing_inspect.is_generic_alias(lhs) - # and _typing_inspect.is_valid_isinstance_arg(rhs) + 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) + return issubtype(_typing_inspect.get_origin(lhs), rhs) + # C <:? D[A] elif bool( _typing_inspect.is_valid_isinstance_arg(lhs) diff --git a/typemap/type_eval/_typing_inspect.py b/typemap/type_eval/_typing_inspect.py index 66a60ed..3da6c61 100644 --- a/typemap/type_eval/_typing_inspect.py +++ b/typemap/type_eval/_typing_inspect.py @@ -21,6 +21,8 @@ from typing_extensions import TypeAliasType, TypeVarTuple, Unpack from types import GenericAlias, UnionType +from . import _eval_typing + 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] @@ -123,6 +125,10 @@ def is_literal(t: Any) -> bool: return is_generic_alias(t) and get_origin(t) is Literal # type: ignore [comparison-overlap] +def is_eval_proxy(t: Any) -> TypeGuard[type[_eval_typing._EvalProxy]]: + return isinstance(t, type) and issubclass(t, _eval_typing._EvalProxy) + + __all__ = ( "is_annotated", "is_classvar", diff --git a/typemap/typing.py b/typemap/typing.py index 5bcfe0f..71740d1 100644 --- a/typemap/typing.py +++ b/typemap/typing.py @@ -153,11 +153,7 @@ def Is(self, arg): lhs, rhs = arg return type_eval.issubtype( type_eval.eval_typing(lhs), - # XXX: This is solidly wrong, we need to eval both sides... - # But eval_typing currently expands generic types out into - # something broken... - # type_eval.eval_typing(rhs), - rhs, + type_eval.eval_typing(rhs), ) From 07b29df56059e05bc18eb89ac453d2b35a3654fc Mon Sep 17 00:00:00 2001 From: "Michael J. Sullivan" Date: Mon, 20 Oct 2025 14:33:26 -0700 Subject: [PATCH 13/19] Fix literal <:? case --- typemap/type_eval/_subtype.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/typemap/type_eval/_subtype.py b/typemap/type_eval/_subtype.py index 1366d6c..800f0ce 100644 --- a/typemap/type_eval/_subtype.py +++ b/typemap/type_eval/_subtype.py @@ -46,7 +46,7 @@ def issubtype(lhs: typing.Any, rhs: typing.Any) -> bool: # literal <:? type elif _typing_inspect.is_literal(lhs): - return issubtype(type(typing.get_args(lhs)[0]), rhs) + return all(issubtype(type(x), rhs) for x in typing.get_args(lhs)) # C[A] <:? D elif bool( From 6af98780c717e49cea29af70704505a685abaa72 Mon Sep 17 00:00:00 2001 From: "Michael J. Sullivan" Date: Mon, 20 Oct 2025 15:16:21 -0700 Subject: [PATCH 14/19] Add more TODO notes --- spec-draft.rst | 31 +++++++++++++++++++++++++++++++ typemap/type_eval/_subtype.py | 11 +++++++++++ 2 files changed, 42 insertions(+) diff --git a/spec-draft.rst b/spec-draft.rst index 8082c58..cea3967 100644 --- a/spec-draft.rst +++ b/spec-draft.rst @@ -82,3 +82,34 @@ We can put more in, but this is what typescript has. * ``Lowercase[S: Literal & str]`` * ``Capitalize[S: Literal & str]`` * ``Uncapitalize[S: Literal & str]`` + + +------------------------------------------------------------------------- + + +Big open questions? + +Can we actually implement Is (IsSubtype) at runtime in a satisfactory way? + - Could we slightly dodge the question by *not* adding the evaluation library to the standard library, and letting the operations be opaque. + + Then we would promise to have a third-party library, which would need to be "fit for purpose" for people to want to use, but would be free of the burden of being canonical? + +There is a lot that needs to happen, like protocols and variance inference and +callable subtyping (which might require matching against type vars...) + +How do we deal with modifiers? ClassVar, Final, Required, ReadOnly + - One option is to treat them not as types by as *modifiers* and have them + in a separate field where they are a union of Literals. + So ``x: Final[ClassVar[int]]`` would appear in ``Attrs`` as + ``Member[Literal['x'], int, Literal['Final' | 'ClassVar']]`` + + +How do we deal with Callables? We need to support extended callable syntax basically. +Or something like it. + + +===== + +This proposal is less "well-typed" than typescript... (Well-kinded, maybe?) +Typescript has better typechecking at the alias definition site: +For ``P[K]``, ``K`` needs to have ``keyof P``... diff --git a/typemap/type_eval/_subtype.py b/typemap/type_eval/_subtype.py index 800f0ce..8c7140a 100644 --- a/typemap/type_eval/_subtype.py +++ b/typemap/type_eval/_subtype.py @@ -68,9 +68,20 @@ def issubtype(lhs: typing.Any, rhs: typing.Any) -> bool: return lhs is rhs # TODO: What to do about C[A] <:? D[B]??? + # TODO: and we will we need to infer variance ourselves with the new syntax # TODO: Protocols??? + # TODO: tuple + + # TODO: Callable -- oh no, and callable needs + + # TODO: Any + + # TODO: Annotated + + # TODO: TypedDict + # TODO: We will need to have some sort of hook to support runtime # checking of typechecker extensions. # From ab1db0f7a15f6529dcafbc6e041aa9f62ba46a2a Mon Sep 17 00:00:00 2001 From: "Michael J. Sullivan" Date: Wed, 22 Oct 2025 16:56:33 -0700 Subject: [PATCH 15/19] More thinking --- spec-draft.rst | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/spec-draft.rst b/spec-draft.rst index cea3967..9890a55 100644 --- a/spec-draft.rst +++ b/spec-draft.rst @@ -58,10 +58,12 @@ only used for Any/All. # TODO: New TypedDict setup * ``NewProtocol[*Ps: Member]`` +* ``Members[T]`` produces a ``tuple`` of ``Member`` types. * ``Member[N: Literal & str, T]`` # These names are too long -- but we can't do ``Type`` !! * ``GetName[T: Member]`` * ``GetType[T: Member]`` +* Could we also put the defining type there?? --- @@ -89,27 +91,53 @@ We can put more in, but this is what typescript has. Big open questions? +1. Can we actually implement Is (IsSubtype) at runtime in a satisfactory way? - Could we slightly dodge the question by *not* adding the evaluation library to the standard library, and letting the operations be opaque. Then we would promise to have a third-party library, which would need to be "fit for purpose" for people to want to use, but would be free of the burden of being canonical? -There is a lot that needs to happen, like protocols and variance inference and + There is a lot that needs to happen, like protocols and variance inference and callable subtyping (which might require matching against type vars...) + - I think we probably *can't* try to put it in the standard library. I think it would by nature bless the implementation with some degree of canonicity that I'm not sure we can back up. Different typecheckers don't always match on subtyping behavior, *and* it sometimes depends on config flags (like strict_optional in mypy). *And* we could imagine a bunch of other config flags: whether to be strict about argument names in protocols, for example. + + - We can instead have something simpler, which I will call ``Matches``. ``Matches`` would do *simple* checking of the *head* of types, essentially, without looking at type parameters. It would still lift over unions and would check literals. + Honestly this is basically what is currently implemented for the examples, so it is probably good enough. + + It's unsatisfying, though. + +2. How do we deal with modifiers? ClassVar, Final, Required, ReadOnly - One option is to treat them not as types by as *modifiers* and have them in a separate field where they are a union of Literals. So ``x: Final[ClassVar[int]]`` would appear in ``Attrs`` as ``Member[Literal['x'], int, Literal['Final' | 'ClassVar']]`` + This is kind of unsatisfying but I think it's probably right. + We could also have a ``MemberUpdate[M: Member, T]`` that updates + the type of a member but preserves its name and modifiers. + + - + +3. How do we deal with Callables? We need to support extended callable syntax basically. Or something like it. +4. +What do we do about ``Members`` on built-in types? ``typing.get_type_hints(int)`` returns ``{}`` but mypy will not agree! + +An object of an empty user-defined class has 29 entries in ``dir`` (all dunders), and ``object()`` has 24. (In 3.14. In 3.12, it was 27 for the user-defined object). ===== This proposal is less "well-typed" than typescript... (Well-kinded, maybe?) Typescript has better typechecking at the alias definition site: For ``P[K]``, ``K`` needs to have ``keyof P``... + +Oh, we could maybe do better but it would require some new machinery. +* ``KeyOf[T]`` - literal keys of ``T`` +* ``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. +* We would also need to do context sensitive type bound inference From 83445b47f4d06564cefe04d366341320109b1475 Mon Sep 17 00:00:00 2001 From: "Michael J. Sullivan" Date: Thu, 23 Oct 2025 11:21:30 -0700 Subject: [PATCH 16/19] Create an istypematch notion to compete with the issubtype idea --- typemap/type_eval/__init__.py | 2 + typemap/type_eval/_subtype.py | 9 ++++ typemap/type_eval/_tmatch.py | 90 +++++++++++++++++++++++++++++++++++ typemap/typing.py | 14 +++++- 4 files changed, 114 insertions(+), 1 deletion(-) create mode 100644 typemap/type_eval/_tmatch.py diff --git a/typemap/type_eval/__init__.py b/typemap/type_eval/__init__.py index 9ce08fa..298483d 100644 --- a/typemap/type_eval/__init__.py +++ b/typemap/type_eval/__init__.py @@ -1,12 +1,14 @@ from ._eval_call import eval_call from ._eval_typing import eval_typing, _get_current_context, _EvalProxy from ._subtype import issubtype +from ._tmatch import istypematch __all__ = ( "eval_typing", "eval_call", "issubtype", + "istypematch", "_EvalProxy", "_get_current_context", ) diff --git a/typemap/type_eval/_subtype.py b/typemap/type_eval/_subtype.py index 8c7140a..0932373 100644 --- a/typemap/type_eval/_subtype.py +++ b/typemap/type_eval/_subtype.py @@ -1,3 +1,9 @@ +# XXX: This is the start of an implementation of issubtype, but +# honestly it is still mostly the same as istypematch. I'm preserving +# it for now and might still expand it some. +# Largely the value of it is in the TODO comments I guess. + + import typing @@ -10,6 +16,9 @@ def issubtype(lhs: typing.Any, rhs: typing.Any) -> bool: # TODO: Need to handle a lot of cases! + # TODO: We will probably need to carry a context around, + # and maybe recursively invoke eval_typing? + # N.B: All of the 'bool's in these are because black otherwise # formats the two-conditional chains in an unconscionably bad way. diff --git a/typemap/type_eval/_tmatch.py b/typemap/type_eval/_tmatch.py new file mode 100644 index 0000000..72dbbe1 --- /dev/null +++ b/typemap/type_eval/_tmatch.py @@ -0,0 +1,90 @@ +import typing + + +from . import _typing_inspect + + +__all__ = ("istypematch",) + + +def istypematch(lhs: typing.Any, rhs: typing.Any) -> bool: + # TODO: Need to handle some 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(istypematch(lhs, r) for r in typing.get_args(rhs)) + elif _typing_inspect.is_union_type(lhs): + return all(istypematch(t, rhs) for t in typing.get_args(lhs)) + + # For _EvalProxy's just blow through them, since we don't yet care + # about the attribute types here. + elif _typing_inspect.is_eval_proxy(lhs): + return istypematch(lhs.__origin__, rhs) + elif _typing_inspect.is_eval_proxy(rhs): + return istypematch(lhs, rhs.__origin__) + + 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 all(istypematch(type(x), rhs) for x in typing.get_args(lhs)) + + # C[A] <:? D + elif bool( + _typing_inspect.is_generic_alias(lhs) + and _typing_inspect.is_valid_isinstance_arg(rhs) + ): + return istypematch(_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 istypematch(lhs, _typing_inspect.get_origin(rhs)) + + # C[A] <:? D[B] -- just match the heads! + elif bool( + _typing_inspect.is_generic_alias(lhs) + and _typing_inspect.is_generic_alias(rhs) + ): + return istypematch( + _typing_inspect.get_origin(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: and we will we need to infer variance ourselves with the new syntax + + # TODO: Protocols??? + + # Check behavior? + # TODO: Annotated + # TODO: tuple + # TODO: Callable + # TODO: Any + # TODO: TypedDict + + # This will often fail -- eventually should return False + return issubclass(lhs, rhs) diff --git a/typemap/typing.py b/typemap/typing.py index 71740d1..aa3652a 100644 --- a/typemap/typing.py +++ b/typemap/typing.py @@ -149,7 +149,7 @@ def GetArg(self, arg): @_SpecialForm -def Is(self, arg): +def IsSubtype(self, arg): lhs, rhs = arg return type_eval.issubtype( type_eval.eval_typing(lhs), @@ -157,6 +157,18 @@ def Is(self, arg): ) +@_SpecialForm +def IsTypematch(self, arg): + lhs, rhs = arg + return type_eval.istypematch( + type_eval.eval_typing(lhs), + type_eval.eval_typing(rhs), + ) + + +Is = IsTypematch + + ################################################################## From 9555e2f28d16af85aa01dda401b2c7dde0cbcda0 Mon Sep 17 00:00:00 2001 From: "Michael J. Sullivan" Date: Tue, 28 Oct 2025 12:13:11 -0700 Subject: [PATCH 17/19] More spec tweaks --- spec-draft.rst | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/spec-draft.rst b/spec-draft.rst index 9890a55..3d7573a 100644 --- a/spec-draft.rst +++ b/spec-draft.rst @@ -59,7 +59,7 @@ only used for Any/All. * ``NewProtocol[*Ps: Member]`` * ``Members[T]`` produces a ``tuple`` of ``Member`` types. -* ``Member[N: Literal & str, T]`` +* ``Member[N: Literal[str], T]`` # These names are too long -- but we can't do ``Type`` !! * ``GetName[T: Member]`` * ``GetType[T: Member]`` @@ -67,7 +67,7 @@ only used for Any/All. --- -* ``GetAttr[T, S: Literal & str]`` +* ``GetAttr[T, S: Literal[str]]`` # TODO: how to deal with special forms like Callable and tuple[T, ...] * ``GetArgs[T]`` - returns a tuple containing all of the type arguments @@ -79,11 +79,19 @@ only used for Any/All. String manipulation operations for string Literal types. We can put more in, but this is what typescript has. +``Slice`` and ``Concat`` are a poor man's literal template. +We can actually implement the case functions in terms of them and a +bunch of conditionals. + + +* ``Slice[S: Literal[str], Start: Literal[int | None], End: Literal[int | None]]`` +* ``Concat[S1: Literal[str], S2: Literal[str]]`` + +* ``Uppercase[S: Literal[str]]`` +* ``Lowercase[S: Literal[str]]`` +* ``Capitalize[S: Literal[str]]`` +* ``Uncapitalize[S: Literal[str]]`` -* ``Uppercase[S: Literal & str]`` -* ``Lowercase[S: Literal & str]`` -* ``Capitalize[S: Literal & str]`` -* ``Uncapitalize[S: Literal & str]`` ------------------------------------------------------------------------- From 8986f1b871af34d6bd4ef64e25be685231ad0463 Mon Sep 17 00:00:00 2001 From: "Michael J. Sullivan" Date: Tue, 28 Oct 2025 12:16:19 -0700 Subject: [PATCH 18/19] Rename istypematch to issubsimilar. I don't know. --- typemap/type_eval/__init__.py | 4 ++-- typemap/type_eval/{_tmatch.py => _subsim.py} | 20 ++++++++++---------- typemap/type_eval/_subtype.py | 2 +- typemap/typing.py | 6 +++--- 4 files changed, 16 insertions(+), 16 deletions(-) rename typemap/type_eval/{_tmatch.py => _subsim.py} (79%) diff --git a/typemap/type_eval/__init__.py b/typemap/type_eval/__init__.py index 298483d..8973014 100644 --- a/typemap/type_eval/__init__.py +++ b/typemap/type_eval/__init__.py @@ -1,14 +1,14 @@ from ._eval_call import eval_call from ._eval_typing import eval_typing, _get_current_context, _EvalProxy from ._subtype import issubtype -from ._tmatch import istypematch +from ._subsim import issubsimilar __all__ = ( "eval_typing", "eval_call", "issubtype", - "istypematch", + "issubsimilar", "_EvalProxy", "_get_current_context", ) diff --git a/typemap/type_eval/_tmatch.py b/typemap/type_eval/_subsim.py similarity index 79% rename from typemap/type_eval/_tmatch.py rename to typemap/type_eval/_subsim.py index 72dbbe1..8304d8d 100644 --- a/typemap/type_eval/_tmatch.py +++ b/typemap/type_eval/_subsim.py @@ -4,10 +4,10 @@ from . import _typing_inspect -__all__ = ("istypematch",) +__all__ = ("issubsimilar",) -def istypematch(lhs: typing.Any, rhs: typing.Any) -> bool: +def issubsimilar(lhs: typing.Any, rhs: typing.Any) -> bool: # TODO: Need to handle some cases # N.B: All of the 'bool's in these are because black otherwise @@ -15,16 +15,16 @@ def istypematch(lhs: typing.Any, rhs: typing.Any) -> bool: # Unions first if _typing_inspect.is_union_type(rhs): - return any(istypematch(lhs, r) for r in typing.get_args(rhs)) + return any(issubsimilar(lhs, r) for r in typing.get_args(rhs)) elif _typing_inspect.is_union_type(lhs): - return all(istypematch(t, rhs) for t in typing.get_args(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 # about the attribute types here. elif _typing_inspect.is_eval_proxy(lhs): - return istypematch(lhs.__origin__, rhs) + return issubsimilar(lhs.__origin__, rhs) elif _typing_inspect.is_eval_proxy(rhs): - return istypematch(lhs, rhs.__origin__) + return issubsimilar(lhs, rhs.__origin__) elif bool( _typing_inspect.is_valid_isinstance_arg(lhs) @@ -45,28 +45,28 @@ def istypematch(lhs: typing.Any, rhs: typing.Any) -> bool: # literal <:? type elif _typing_inspect.is_literal(lhs): - return all(istypematch(type(x), rhs) for x in typing.get_args(lhs)) + return all(issubsimilar(type(x), rhs) for x in typing.get_args(lhs)) # C[A] <:? D elif bool( _typing_inspect.is_generic_alias(lhs) and _typing_inspect.is_valid_isinstance_arg(rhs) ): - return istypematch(_typing_inspect.get_origin(lhs), rhs) + return issubsimilar(_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 istypematch(lhs, _typing_inspect.get_origin(rhs)) + return issubsimilar(lhs, _typing_inspect.get_origin(rhs)) # C[A] <:? D[B] -- just match the heads! elif bool( _typing_inspect.is_generic_alias(lhs) and _typing_inspect.is_generic_alias(rhs) ): - return istypematch( + return issubsimilar( _typing_inspect.get_origin(lhs), _typing_inspect.get_origin(rhs) ) diff --git a/typemap/type_eval/_subtype.py b/typemap/type_eval/_subtype.py index 0932373..a80313c 100644 --- a/typemap/type_eval/_subtype.py +++ b/typemap/type_eval/_subtype.py @@ -1,5 +1,5 @@ # XXX: This is the start of an implementation of issubtype, but -# honestly it is still mostly the same as istypematch. I'm preserving +# honestly it is still mostly the same as issubsimilar. I'm preserving # it for now and might still expand it some. # Largely the value of it is in the TODO comments I guess. diff --git a/typemap/typing.py b/typemap/typing.py index aa3652a..208d8f5 100644 --- a/typemap/typing.py +++ b/typemap/typing.py @@ -158,15 +158,15 @@ def IsSubtype(self, arg): @_SpecialForm -def IsTypematch(self, arg): +def IsSubSimilar(self, arg): lhs, rhs = arg - return type_eval.istypematch( + return type_eval.issubsimilar( type_eval.eval_typing(lhs), type_eval.eval_typing(rhs), ) -Is = IsTypematch +Is = IsSubSimilar ################################################################## From 4bbd6978a0e91a265493ef34df1ac95d3bf433a4 Mon Sep 17 00:00:00 2001 From: "Michael J. Sullivan" Date: Tue, 28 Oct 2025 16:07:40 -0700 Subject: [PATCH 19/19] Start expanding Attrs, Members --- spec-draft.rst | 9 +++- tests/test_type_dir.py | 22 +++++++++ typemap/typing.py | 106 +++++++++++++++++++++++++++++++++++++++-- 3 files changed, 133 insertions(+), 4 deletions(-) diff --git a/spec-draft.rst b/spec-draft.rst index 3d7573a..afbf6df 100644 --- a/spec-draft.rst +++ b/spec-draft.rst @@ -59,10 +59,12 @@ only used for Any/All. * ``NewProtocol[*Ps: Member]`` * ``Members[T]`` produces a ``tuple`` of ``Member`` types. -* ``Member[N: Literal[str], T]`` +* ``Member[N: Literal[str], T, Q: Quals, D]`` # These names are too long -- but we can't do ``Type`` !! * ``GetName[T: Member]`` * ``GetType[T: Member]`` +* ``GetQuals[T: Member]`` +* ``GetDefiner[T: Member]`` * Could we also put the defining type there?? --- @@ -138,6 +140,11 @@ What do we do about ``Members`` on built-in types? ``typing.get_type_hints(int)` An object of an empty user-defined class has 29 entries in ``dir`` (all dunders), and ``object()`` has 24. (In 3.14. In 3.12, it was 27 for the user-defined object). +5. +Polymorphic callables? How do we represent their type and how do we construct their type? + +What does TS do here? - TS has full impredactive polymorphic functions. You can do System F stuff. + ===== This proposal is less "well-typed" than typescript... (Well-kinded, maybe?) diff --git a/tests/test_type_dir.py b/tests/test_type_dir.py index 7bc1d99..40f861d 100644 --- a/tests/test_type_dir.py +++ b/tests/test_type_dir.py @@ -9,6 +9,7 @@ GetType, Iter, Attrs, + Members, FromUnion, Uppercase, Is, @@ -31,11 +32,15 @@ class AnotherBase[I]: class Base[T]: + # This K is dodgy K = TypeVar("K") t: dict[str, StrForInt[T]] kkk: K + def foo(self, a: T | None, b: int = 0) -> dict[str, T]: + pass + def base[Z](self, a: T | Z | None, b: K) -> dict[str, T | Z]: pass @@ -144,6 +149,7 @@ class Final: kkk: ~K x: tests.test_type_dir.Wrapper[int | None] ordinary: str + def foo(self, a: int | None, b: int = 0) -> dict[str, int]: ... def base[Z](self, a: int | Z | None, b: ~K) -> dict[str, int | Z]: ... def cbase(cls, a: int | None, b: ~K) -> dict[str, int]: ... def sbase[Z](cls, a: int | Literal['gotcha!'] | Z | None, b: ~K) -> dict[str, int | Z]: ... @@ -217,3 +223,19 @@ class NoLiterals2[tests.test_type_dir.Final]: x: tests.test_type_dir.Wrapper[int | None] ordinary: str """) + + +def test_type_dir_7(): + d = eval_typing(Members[Final]) + foo = next(iter(m for m in Iter[d] if m.__args__[0].__args__[0] == "foo")) + # XXX: drop self? + assert ( + str(foo) + == "\ +typemap.typing.Member[typing.Literal['foo'], \ +typing.Callable[[\ +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]" + ) diff --git a/typemap/typing.py b/typemap/typing.py index 208d8f5..ad05100 100644 --- a/typemap/typing.py +++ b/typemap/typing.py @@ -76,23 +76,121 @@ def _from_literal(val): return val -class Member[N: str, T]: +class Member[N: str, T, Q: str = typing.Never, D = typing.Never]: pass type GetName[T: Member] = GetArg[T, 0] # type: ignore[valid-type] type GetType[T: Member] = GetArg[T, 1] # type: ignore[valid-type] +type GetQuals[T: Member] = GetArg[T, 2] # type: ignore[valid-type] +type GetDefiner[T: Member] = GetArg[T, 3] # type: ignore[valid-type] ################################################################## +def get_annotated_type_hints(cls, **kwargs): + """Get the type hints for a cls annotated with definition site. + + This traverses the mro and finds the definition site for each annotation. + """ + ohints = typing.get_type_hints(cls, **kwargs) + hints = {} + for acls in cls.__mro__: + if not hasattr(acls, "__annotations__"): + continue + for k in acls.__annotations__: + if k not in hints: + hints[k] = ohints[k], acls + + # Stop early if we are done. + if len(hints) == len(ohints): + break + return hints + + @_SpecialForm def Attrs(self, tp): # TODO: Support unions o = type_eval.eval_typing(tp) - hints = typing.get_type_hints(o, include_extras=True) - return tuple[*[Member[typing.Literal[n], t] for n, t in hints.items()]] + hints = get_annotated_type_hints(o, include_extras=True) + + return tuple[ + *[ + Member[typing.Literal[n], t, typing.Never, d] + for n, (t, d) in hints.items() + ] + ] + + +class Param[N: str | None, T, Q: str = typing.Never]: + pass + + +def _function_type(func, *, is_method): + root = inspect.unwrap(func) + sig = inspect.signature(root) + # XXX: __type_params__!!! + + empty = inspect.Parameter.empty + + def _ann(x): + return typing.Any if x is empty else x + + params = [] + for _i, p in enumerate(sig.parameters.values()): + # XXX: what should we do about self? + # should we track classmethod/staticmethod somehow? + # if i == 0 and is_method: + # continue + has_name = p.kind in ( + inspect.Parameter.POSITIONAL_OR_KEYWORD, + inspect.Parameter.KEYWORD_ONLY, + ) + quals = [] + if p.kind == inspect.Parameter.VAR_POSITIONAL: + quals.append("*") + if p.kind == inspect.Parameter.VAR_KEYWORD: + quals.append("**") + if p.default is not empty: + quals.append("=") + params.append( + Param[ + typing.Literal[p.name if has_name else None], + _ann(p.annotation), + typing.Literal[*quals] if quals else typing.Never, + ] + ) + + return typing.Callable[params, _ann(sig.return_annotation)] + + +@_SpecialForm +def Members(self, tp): + # TODO: Support unions + o = type_eval.eval_typing(tp) + hints = get_annotated_type_hints(o, include_extras=True) + + attrs = [ + Member[typing.Literal[n], t, typing.Never, d] + for n, (t, d) in hints.items() + ] + + for name, attr in o.__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] ################################################################## @@ -171,6 +269,8 @@ def IsSubSimilar(self, arg): ################################################################## +# TODO: unions! Slice, Concat + class _StringLiteralOp: def __init__(self, op: typing.Callable[[str], str]):