From 63c02c879cd0b492f52d70b1074629ad9f3873c3 Mon Sep 17 00:00:00 2001 From: "Michael J. Sullivan" Date: Wed, 4 Feb 2026 17:49:11 -0800 Subject: [PATCH] Blow away IsSubsimilar The view seems to be that the real thing needs to be full python typing assignability, and the runtime will just need to do some the best it can. --- README.md | 2 +- design-qs.rst | 46 -------------- tests/test_type_eval.py | 11 ---- typemap/type_eval/__init__.py | 2 - typemap/type_eval/_eval_operators.py | 23 +++---- typemap/type_eval/_subsim.py | 94 ---------------------------- typemap/type_eval/_subtype.py | 46 +++++++++----- typemap/typing.py | 10 +-- 8 files changed, 41 insertions(+), 193 deletions(-) delete mode 100644 design-qs.rst delete mode 100644 typemap/type_eval/_subsim.py diff --git a/README.md b/README.md index c585ca7..69f6b9d 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Computed Types in Python -See [pep.rst](pep.rst) for the PEP draft and [design-qs.rst](design-qs.rst) for some design discussion not yet merged into the PEP. +See [pep.rst](pep.rst) for the PEP draft. ## Development diff --git a/design-qs.rst b/design-qs.rst deleted file mode 100644 index 4d4e50b..0000000 --- a/design-qs.rst +++ /dev/null @@ -1,46 +0,0 @@ -Big (open?) questions ---------------------- - -1. -Can we actually implement IsSubtype at runtime in a satisfactory way? -(PROBABLE DECISION: external library *and* "full" best effort checking.) - - - There is a lot that needs to happen, like protocols and variance - inference and callable subtyping (which might require matching - against type vars...). Jukka points out that lots of type - information is frequently missing at runtime too: attributes might - be inferred, which is a *feature*. - - - 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? - - - 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 - ``IsSubSimilar``. ``IsSubSimilar`` 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. - - Probably need a better name. - - Honestly this is basically what is currently implemented for the - examples, so it is probably good enough. - - It's unsatisfying, though. - - - ----- diff --git a/tests/test_type_eval.py b/tests/test_type_eval.py index 4cf7072..779446c 100644 --- a/tests/test_type_eval.py +++ b/tests/test_type_eval.py @@ -33,7 +33,6 @@ GetSpecialAttr, GetType, GetAnnotations, - IsSubtype, IsSub, Iter, Length, @@ -1408,16 +1407,6 @@ def test_eval_bool_literal_07(): d = eval_typing(IsSub[Literal[False], _BoolLiteral[False]]) assert d == _BoolLiteral[True] - d = eval_typing(IsSubtype[_BoolLiteral[True], Literal[True]]) - assert d == _BoolLiteral[True] - d = eval_typing(IsSubtype[_BoolLiteral[False], Literal[False]]) - assert d == _BoolLiteral[True] - - d = eval_typing(IsSubtype[Literal[True], _BoolLiteral[True]]) - assert d == _BoolLiteral[True] - d = eval_typing(IsSubtype[Literal[False], _BoolLiteral[False]]) - assert d == _BoolLiteral[True] - d = eval_typing(Matches[_BoolLiteral[True], Literal[True]]) assert d == _BoolLiteral[True] d = eval_typing(Matches[_BoolLiteral[False], Literal[False]]) diff --git a/typemap/type_eval/__init__.py b/typemap/type_eval/__init__.py index 3c1484e..7e11eb1 100644 --- a/typemap/type_eval/__init__.py +++ b/typemap/type_eval/__init__.py @@ -9,7 +9,6 @@ # XXX: this needs to go second due to nasty circularity -- try to fix that!! from ._eval_call import eval_call, eval_call_with_types from ._subtype import issubtype -from ._subsim import issubsimilar # This one is imported for registering handlers from . import _eval_operators # noqa @@ -24,7 +23,6 @@ "eval_call_with_types", "flatten_class", "issubtype", - "issubsimilar", "TypeMapError", "_EvalProxy", "_get_current_context", diff --git a/typemap/type_eval/_eval_operators.py b/typemap/type_eval/_eval_operators.py index 1b50c81..38b95a2 100644 --- a/typemap/type_eval/_eval_operators.py +++ b/typemap/type_eval/_eval_operators.py @@ -29,8 +29,7 @@ GetMemberType, GetSpecialAttr, InitField, - IsSubSimilar, - IsSubtype, + IsSub, Iter, Length, Lowercase, @@ -236,23 +235,17 @@ def _eval_Iter(tp, *, ctx): # N.B: These handle unions on their own -@type_eval.register_evaluator(IsSubtype) +@type_eval.register_evaluator(IsSub) @_lift_evaluated -def _eval_IsSubtype(lhs, rhs, *, ctx): +def _eval_IsSub(lhs, rhs, *, ctx): return _BoolLiteral[type_eval.issubtype(lhs, rhs)] -@type_eval.register_evaluator(IsSubSimilar) -@_lift_evaluated -def _eval_IsSubSimilar(lhs, rhs, *, ctx): - return _BoolLiteral[type_eval.issubsimilar(lhs, rhs)] - - @type_eval.register_evaluator(Matches) @_lift_evaluated def _eval_Matches(lhs, rhs, *, ctx): return _BoolLiteral[ - type_eval.issubsimilar(lhs, rhs) and type_eval.issubsimilar(rhs, lhs) + type_eval.issubtype(lhs, rhs) and type_eval.issubtype(rhs, lhs) ] @@ -262,8 +255,8 @@ def _eval_bool_tp(tp, ctx): else: return _BoolLiteral[ any( - type_eval.issubsimilar(arg, typing.Literal[True]) - and not type_eval.issubsimilar(arg, typing.Never) + type_eval.issubtype(arg, typing.Literal[True]) + and not type_eval.issubtype(arg, typing.Never) for arg in _union_elems(tp, ctx) ) ] @@ -1032,7 +1025,7 @@ def _eval_RaiseError(msg, *extra_types, ctx): def _add_quals(typ, quals): for qual in (typing.ClassVar, typing.Final): - if type_eval.issubsimilar(typing.Literal[qual.__name__], quals): + if type_eval.issubtype(typing.Literal[qual.__name__], quals): typ = qual[typ] return typ @@ -1079,7 +1072,7 @@ def _eval_NewProtocol(*etyps: Member, ctx): typ = _eval_types(typ, ctx) tquals = _eval_types(quals, ctx) - if type_eval.issubsimilar( + if type_eval.issubtype( typing.Literal["ClassVar"], tquals ) and _is_method_like(typ): dct[name] = _callable_type_to_method(name, typ) diff --git a/typemap/type_eval/_subsim.py b/typemap/type_eval/_subsim.py deleted file mode 100644 index e87d5b7..0000000 --- a/typemap/type_eval/_subsim.py +++ /dev/null @@ -1,94 +0,0 @@ -import typing - -from . import _typing_inspect - -__all__ = ("issubsimilar",) - - -def issubsimilar(lhs: typing.Any, rhs: typing.Any) -> bool: - # TODO: Need to handle some cases - - if lhs is None: - lhs = type(None) - if rhs is None: - rhs = type(None) - - # 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 lhs is typing.Never: - return True - elif rhs is typing.Never: - return False - - elif _typing_inspect.is_union_type(rhs): - return any(issubsimilar(lhs, r) for r in typing.get_args(rhs)) - elif _typing_inspect.is_union_type(lhs): - 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 issubsimilar(lhs.__origin__, rhs) - elif _typing_inspect.is_eval_proxy(rhs): - return issubsimilar(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 _typing_inspect.is_literal(lhs) and _typing_inspect.is_literal(rhs): - # We need to check both value and type, since True == 1 but - # Literal[True] should not be a subtype of Literal[1] - rhs_args = {(t, type(t)) for t in typing.get_args(rhs)} - return all((lv, type(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(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 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 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 issubsimilar( - _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 - - # 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/type_eval/_subtype.py b/typemap/type_eval/_subtype.py index a80313c..8011290 100644 --- a/typemap/type_eval/_subtype.py +++ b/typemap/type_eval/_subtype.py @@ -1,9 +1,3 @@ -# XXX: This is the start of an implementation of issubtype, but -# 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. - - import typing @@ -15,6 +9,7 @@ def issubtype(lhs: typing.Any, rhs: typing.Any) -> bool: # TODO: Need to handle a lot of cases! + # This is explicitly "best-effort", though. # TODO: We will probably need to carry a context around, # and maybe recursively invoke eval_typing? @@ -22,7 +17,16 @@ def issubtype(lhs: typing.Any, rhs: typing.Any) -> bool: # N.B: All of the 'bool's in these are because black otherwise # formats the two-conditional chains in an unconscionably bad way. + if lhs is None: + lhs = type(None) + if rhs is None: + rhs = type(None) + # Unions first + if lhs is typing.Never: + return True + elif rhs is typing.Never: + return False 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): @@ -43,11 +47,11 @@ def issubtype(lhs: typing.Any, rhs: typing.Any) -> bool: 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)) + elif _typing_inspect.is_literal(lhs) and _typing_inspect.is_literal(rhs): + # We need to check both value and type, since True == 1 but + # Literal[True] should not be a subtype of Literal[1] + rhs_args = {(t, type(t)) for t in typing.get_args(rhs)} + return all((lv, type(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: @@ -71,14 +75,23 @@ def issubtype(lhs: typing.Any, rhs: typing.Any) -> bool: ): return issubtype(lhs, _typing_inspect.get_origin(rhs)) + # C[A] <:? D[B] -- just match the heads! + # Super wrong! + # TODO: What to do about C[A] <:? D[B]??? + # TODO: and we will we need to infer variance ourselves with the new syntax + elif bool( + _typing_inspect.is_generic_alias(lhs) + and _typing_inspect.is_generic_alias(rhs) + ): + return issubtype( + _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: 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 @@ -97,4 +110,7 @@ def issubtype(lhs: typing.Any, rhs: typing.Any) -> bool: # We could have restrictions if we are willing to document them. # This will probably fail - return issubclass(lhs, rhs) + try: + return issubclass(lhs, rhs) + except TypeError as e: + raise TypeError(*e.args, lhs, rhs) diff --git a/typemap/typing.py b/typemap/typing.py index f765487..951f0d6 100644 --- a/typemap/typing.py +++ b/typemap/typing.py @@ -280,12 +280,7 @@ def __bool__(self): @_SpecialForm -def IsSubtype(self, tps): - return _BoolGenericAlias(self, tps) - - -@_SpecialForm -def IsSubSimilar(self, tps): +def IsSub(self, tps): return _BoolGenericAlias(self, tps) @@ -294,9 +289,6 @@ def Matches(self, tps): return _BoolGenericAlias(self, tps) -IsSub = IsSubSimilar - - @_SpecialForm def Bool(self, tp): return _BoolGenericAlias(self, tp)