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)