From 1561ed8c4308fdc0e61cd9fb5432169fa34a5aea Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Fri, 18 Nov 2022 20:20:27 +0000 Subject: [PATCH 1/4] Allow function arguments as base classes --- mypy/checker.py | 29 +++-------------------------- mypy/semanal.py | 25 +++++++++++++++++++++---- mypy/stubtest.py | 2 +- mypy/typeanal.py | 8 ++++++++ mypy/types.py | 28 ++++++++++++++++++++++++++++ test-data/unit/check-classes.test | 12 ++++++++++++ test-data/unit/semanal-types.test | 2 ++ 7 files changed, 75 insertions(+), 31 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index c7de4911501a8..7a66a9408ee42 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -184,7 +184,6 @@ LiteralType, NoneType, Overloaded, - ParamSpecType, PartialType, ProperType, StarType, @@ -203,7 +202,6 @@ UnboundType, UninhabitedType, UnionType, - UnpackType, flatten_nested_unions, get_proper_type, get_proper_types, @@ -211,6 +209,7 @@ is_named_instance, is_optional, remove_optional, + store_argument_type, strip_type, ) from mypy.typetraverser import TypeTraverserVisitor @@ -1174,30 +1173,8 @@ def check_func_def( if ctx.line < 0: ctx = typ self.fail(message_registry.FUNCTION_PARAMETER_CANNOT_BE_COVARIANT, ctx) - if typ.arg_kinds[i] == nodes.ARG_STAR: - if isinstance(arg_type, ParamSpecType): - pass - elif isinstance(arg_type, UnpackType): - if isinstance(get_proper_type(arg_type.type), TupleType): - # Instead of using Tuple[Unpack[Tuple[...]]], just use - # Tuple[...] - arg_type = arg_type.type - else: - arg_type = TupleType( - [arg_type], - fallback=self.named_generic_type( - "builtins.tuple", [self.named_type("builtins.object")] - ), - ) - else: - # builtins.tuple[T] is typing.Tuple[T, ...] - arg_type = self.named_generic_type("builtins.tuple", [arg_type]) - elif typ.arg_kinds[i] == nodes.ARG_STAR2: - if not isinstance(arg_type, ParamSpecType) and not typ.unpack_kwargs: - arg_type = self.named_generic_type( - "builtins.dict", [self.str_type(), arg_type] - ) - item.arguments[i].variable.type = arg_type + # Need to store arguments again for the expanded item. + store_argument_type(item, i, typ, self.named_generic_type) # Type check initialization expressions. body_is_trivial = is_trivial_body(defn.body) diff --git a/mypy/semanal.py b/mypy/semanal.py index 538e37c030a96..a5ddcc70eed63 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -273,6 +273,7 @@ get_proper_types, invalid_recursive_alias, is_named_instance, + store_argument_type, ) from mypy.typevars import fill_typevars from mypy.util import ( @@ -1315,7 +1316,10 @@ def analyze_function_body(self, defn: FuncItem) -> None: # Bind the type variables again to visit the body. if defn.type: a = self.type_analyzer() - a.bind_function_type_variables(cast(CallableType, defn.type), defn) + typ = cast(CallableType, defn.type) + a.bind_function_type_variables(typ, defn) + for i in range(len(typ.arg_types)): + store_argument_type(defn, i, typ, self.named_type) self.function_stack.append(defn) with self.enter(defn): for arg in defn.arguments: @@ -2018,7 +2022,9 @@ def analyze_base_classes( continue try: - base = self.expr_to_analyzed_type(base_expr, allow_placeholder=True) + base = self.expr_to_analyzed_type( + base_expr, allow_placeholder=True, allow_type_any=True + ) except TypeTranslationError: name = self.get_name_repr_of_expr(base_expr) if isinstance(base_expr, CallExpr): @@ -6139,7 +6145,11 @@ def accept(self, node: Node) -> None: report_internal_error(err, self.errors.file, node.line, self.errors, self.options) def expr_to_analyzed_type( - self, expr: Expression, report_invalid_types: bool = True, allow_placeholder: bool = False + self, + expr: Expression, + report_invalid_types: bool = True, + allow_placeholder: bool = False, + allow_type_any: bool = False, ) -> Type | None: if isinstance(expr, CallExpr): # This is a legacy syntax intended mostly for Python 2, we keep it for @@ -6164,7 +6174,10 @@ def expr_to_analyzed_type( return TupleType(info.tuple_type.items, fallback=fallback) typ = self.expr_to_unanalyzed_type(expr) return self.anal_type( - typ, report_invalid_types=report_invalid_types, allow_placeholder=allow_placeholder + typ, + report_invalid_types=report_invalid_types, + allow_placeholder=allow_placeholder, + allow_type_any=allow_type_any, ) def analyze_type_expr(self, expr: Expression) -> None: @@ -6188,6 +6201,7 @@ def type_analyzer( allow_param_spec_literals: bool = False, report_invalid_types: bool = True, prohibit_self_type: str | None = None, + allow_type_any: bool = False, ) -> TypeAnalyser: if tvar_scope is None: tvar_scope = self.tvar_scope @@ -6204,6 +6218,7 @@ def type_analyzer( allow_required=allow_required, allow_param_spec_literals=allow_param_spec_literals, prohibit_self_type=prohibit_self_type, + allow_type_any=allow_type_any, ) tpan.in_dynamic_func = bool(self.function_stack and self.function_stack[-1].is_dynamic()) tpan.global_scope = not self.type and not self.function_stack @@ -6224,6 +6239,7 @@ def anal_type( allow_param_spec_literals: bool = False, report_invalid_types: bool = True, prohibit_self_type: str | None = None, + allow_type_any: bool = False, third_pass: bool = False, ) -> Type | None: """Semantically analyze a type. @@ -6260,6 +6276,7 @@ def anal_type( allow_param_spec_literals=allow_param_spec_literals, report_invalid_types=report_invalid_types, prohibit_self_type=prohibit_self_type, + allow_type_any=allow_type_any, ) tag = self.track_incomplete_refs() typ = typ.accept(a) diff --git a/mypy/stubtest.py b/mypy/stubtest.py index 87ccbd3176df4..fbc7bb478d7b6 100644 --- a/mypy/stubtest.py +++ b/mypy/stubtest.py @@ -354,7 +354,7 @@ def _verify_final( ) -> Iterator[Error]: try: - class SubClass(runtime): # type: ignore[misc,valid-type] + class SubClass(runtime): pass except TypeError: diff --git a/mypy/typeanal.py b/mypy/typeanal.py index 18a63011c5bf4..fc84744c8ceab 100644 --- a/mypy/typeanal.py +++ b/mypy/typeanal.py @@ -201,6 +201,7 @@ def __init__( allow_param_spec_literals: bool = False, report_invalid_types: bool = True, prohibit_self_type: str | None = None, + allow_type_any: bool = False, ) -> None: self.api = api self.lookup_qualified = api.lookup_qualified @@ -237,6 +238,8 @@ def __init__( # Names of type aliases encountered while analysing a type will be collected here. self.aliases_used: set[str] = set() self.prohibit_self_type = prohibit_self_type + # Allow variables typed as Type[Any] and type (useful for base classes). + self.allow_type_any = allow_type_any def visit_unbound_type(self, t: UnboundType, defining_literal: bool = False) -> Type: typ = self.visit_unbound_type_nonoptional(t, defining_literal) @@ -730,6 +733,11 @@ def analyze_unbound_type_without_type_info( return AnyType( TypeOfAny.from_unimported_type, missing_import_name=typ.missing_import_name ) + elif self.allow_type_any: + if isinstance(typ, Instance) and typ.type.fullname == "builtins.type": + return AnyType(TypeOfAny.special_form) + if isinstance(typ, TypeType) and isinstance(typ.item, AnyType): + return AnyType(TypeOfAny.from_another_any, source_any=typ.item) # Option 2: # Unbound type variable. Currently these may be still valid, # for example when defining a generic type alias. diff --git a/mypy/types.py b/mypy/types.py index 1de294f9952d8..2ce38add63d01 100644 --- a/mypy/types.py +++ b/mypy/types.py @@ -7,6 +7,7 @@ from typing import ( TYPE_CHECKING, Any, + Callable, ClassVar, Dict, Iterable, @@ -29,6 +30,7 @@ ArgKind, FakeInfo, FuncDef, + FuncItem, SymbolNode, ) from mypy.state import state @@ -3390,3 +3392,29 @@ def callable_with_ellipsis(any_type: AnyType, ret_type: Type, fallback: Instance fallback=fallback, is_ellipsis_args=True, ) + + +def store_argument_type( + defn: FuncItem, i: int, typ: CallableType, named_type: Callable[[str, list[Type]], Instance] +) -> None: + arg_type = typ.arg_types[i] + if typ.arg_kinds[i] == ARG_STAR: + if isinstance(arg_type, ParamSpecType): + pass + elif isinstance(arg_type, UnpackType): + if isinstance(get_proper_type(arg_type.type), TupleType): + # Instead of using Tuple[Unpack[Tuple[...]]], just use + # Tuple[...] + arg_type = arg_type.type + else: + arg_type = TupleType( + [arg_type], + fallback=named_type("builtins.tuple", [named_type("builtins.object", [])]), + ) + else: + # builtins.tuple[T] is typing.Tuple[T, ...] + arg_type = named_type("builtins.tuple", [arg_type]) + elif typ.arg_kinds[i] == ARG_STAR2: + if not isinstance(arg_type, ParamSpecType) and not typ.unpack_kwargs: + arg_type = named_type("builtins.dict", [named_type("builtins.str", []), arg_type]) + defn.arguments[i].variable.type = arg_type diff --git a/test-data/unit/check-classes.test b/test-data/unit/check-classes.test index 33208c081c28c..e465fd8be00d5 100644 --- a/test-data/unit/check-classes.test +++ b/test-data/unit/check-classes.test @@ -7664,3 +7664,15 @@ class C(B): def foo(self) -> int: # E: Signature of "foo" incompatible with supertype "B" ... [builtins fixtures/property.pyi] + +[case testAllowArgumentAsBaseClass] +from typing import Any, Type + +def f(b: Any) -> None: + class D(b): ... + +def g(b: Type[Any]) -> None: + class D(b): ... + +def h(b: type) -> None: + class D(b): ... diff --git a/test-data/unit/semanal-types.test b/test-data/unit/semanal-types.test index d832772f5f81e..8dc767e1abfcf 100644 --- a/test-data/unit/semanal-types.test +++ b/test-data/unit/semanal-types.test @@ -790,6 +790,7 @@ def f(x: int) -> None: pass def f(*args) -> None: pass x = f +[builtins fixtures/tuple.pyi] [out] MypyFile:1( ImportFrom:1(typing, [overload]) @@ -1032,6 +1033,7 @@ MypyFile:1( [case testVarArgsAndKeywordArgs] def g(*x: int, y: str = ''): pass +[builtins fixtures/tuple.pyi] [out] MypyFile:1( FuncDef:1( From 5cfbfb4351d91a69770ff31b5e717fa70fcb9367 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Fri, 18 Nov 2022 20:24:11 +0000 Subject: [PATCH 2/4] Improve test --- test-data/unit/check-classes.test | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test-data/unit/check-classes.test b/test-data/unit/check-classes.test index e465fd8be00d5..e3aea122ebe1b 100644 --- a/test-data/unit/check-classes.test +++ b/test-data/unit/check-classes.test @@ -7668,6 +7668,9 @@ class C(B): [case testAllowArgumentAsBaseClass] from typing import Any, Type +def e(b) -> None: + class D(b): ... + def f(b: Any) -> None: class D(b): ... From 49e9ab3b2a58df960fc48e018234a3dd2b7bd93d Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Fri, 18 Nov 2022 23:25:00 +0000 Subject: [PATCH 3/4] Fix (+ unrelated bug with a test) --- mypy/stubtest.py | 2 +- mypy/treetransform.py | 2 +- test-data/unit/check-python38.test | 16 ++++++++++++++++ 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/mypy/stubtest.py b/mypy/stubtest.py index fbc7bb478d7b6..74e57d9e5617b 100644 --- a/mypy/stubtest.py +++ b/mypy/stubtest.py @@ -354,7 +354,7 @@ def _verify_final( ) -> Iterator[Error]: try: - class SubClass(runtime): + class SubClass(runtime): # type: ignore[misc] pass except TypeError: diff --git a/mypy/treetransform.py b/mypy/treetransform.py index c863db6b3dd56..2f678b89b1e6e 100644 --- a/mypy/treetransform.py +++ b/mypy/treetransform.py @@ -550,7 +550,7 @@ def visit_super_expr(self, node: SuperExpr) -> SuperExpr: return new def visit_assignment_expr(self, node: AssignmentExpr) -> AssignmentExpr: - return AssignmentExpr(node.target, node.value) + return AssignmentExpr(self.expr(node.target), self.expr(node.value)) def visit_unary_expr(self, node: UnaryExpr) -> UnaryExpr: new = UnaryExpr(node.op, self.expr(node.expr)) diff --git a/test-data/unit/check-python38.test b/test-data/unit/check-python38.test index 1922192c2877a..30bdadf900c3a 100644 --- a/test-data/unit/check-python38.test +++ b/test-data/unit/check-python38.test @@ -718,3 +718,19 @@ def f1() -> None: y = x z = x [builtins fixtures/dict.pyi] + +[case testNarrowOnSelfInGeneric] +# flags: --strict-optional +from typing import Generic, TypeVar, Optional + +T = TypeVar("T", int, str) + +class C(Generic[T]): + x: Optional[T] + def meth(self) -> Optional[T]: + if (y := self.x) is not None: + reveal_type(y) + return None +[out] +main:10: note: Revealed type is "builtins.int" +main:10: note: Revealed type is "builtins.str" From 1d8c773404b604a4b7abe2dfb7195858a17badec Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Sat, 19 Nov 2022 11:13:15 +0000 Subject: [PATCH 4/4] Add test case for another accidentally fixed bug --- test-data/unit/check-selftype.test | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/test-data/unit/check-selftype.test b/test-data/unit/check-selftype.test index 494ae54400fbb..b002746a33972 100644 --- a/test-data/unit/check-selftype.test +++ b/test-data/unit/check-selftype.test @@ -1772,3 +1772,16 @@ class D(C): ... reveal_type(D.f) # N: Revealed type is "def [T] (T`-1) -> T`-1" reveal_type(D().f) # N: Revealed type is "def () -> __main__.D" + +[case testTypingSelfOnSuperTypeVarValues] +from typing import Self, Generic, TypeVar + +T = TypeVar("T", int, str) + +class B: + def copy(self) -> Self: ... +class C(B, Generic[T]): + def copy(self) -> Self: + inst = super().copy() + reveal_type(inst) # N: Revealed type is "Self`0" + return inst