diff --git a/mypy/checker.py b/mypy/checker.py index be365dcdb2aa..a3fd226e1d21 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -26,7 +26,7 @@ ARG_POS, ARG_STAR, LITERAL_TYPE, MDEF, GDEF, CONTRAVARIANT, COVARIANT, INVARIANT, TypeVarExpr, AssignmentExpr, is_final_node, -) + ARG_NAMED) from mypy import nodes from mypy.literals import literal, literal_hash from mypy.typeanal import has_any_from_unimported_type, check_for_explicit_any @@ -1670,6 +1670,7 @@ def visit_class_def(self, defn: ClassDef) -> None: with self.scope.push_class(defn.info): self.accept(defn.defs) self.binder = old_binder + self.check_init_subclass(defn) if not defn.has_incompatible_baseclass: # Otherwise we've already found errors; more errors are not useful self.check_multiple_inheritance(typ) @@ -1699,6 +1700,50 @@ def visit_class_def(self, defn: ClassDef) -> None: if typ.is_protocol and typ.defn.type_vars: self.check_protocol_variance(defn) + def check_init_subclass(self, defn: ClassDef) -> None: + """Check that keywords in a class definition are valid arguments for __init_subclass__(). + + In this example: + 1 class Base: + 2 def __init_subclass__(cls, thing: int): + 3 pass + 4 class Child(Base, thing=5): + 5 def __init_subclass__(cls): + 6 pass + 7 Child() + + Base.__init_subclass__(thing=5) is called at line 4. This is what we simulate here. + Child.__init_subclass__ is never called. + """ + # At runtime, only Base.__init_subclass__ will be called, so + # we skip the current class itself. + for base in defn.info.mro[1:]: + if '__init_subclass__' not in base.names: + continue + name_expr = NameExpr(defn.name) + name_expr.node = base + callee = MemberExpr(name_expr, '__init_subclass__') + args = list(defn.keywords.values()) + arg_names = list(defn.keywords.keys()) # type: List[Optional[str]] + # 'metaclass' keyword is consumed by the rest of the type machinery, + # and is never passed to __init_subclass__ implementations + if 'metaclass' in arg_names: + idx = arg_names.index('metaclass') + arg_names.pop(idx) + args.pop(idx) + arg_kinds = [ARG_NAMED] * len(args) + call_expr = CallExpr(callee, args, arg_kinds, arg_names) + call_expr.line = defn.line + call_expr.column = defn.column + call_expr.end_line = defn.end_line + self.expr_checker.accept(call_expr, + allow_none_return=True, + always_allow_any=True) + # We are only interested in the first Base having __init_subclass__ + # all other bases have already been checked. + break + return + def check_protocol_variance(self, defn: ClassDef) -> None: """Check that protocol definition is compatible with declared variances of type variables. diff --git a/test-data/unit/check-classes.test b/test-data/unit/check-classes.test index 90968f75697e..2c05532c4f9e 100644 --- a/test-data/unit/check-classes.test +++ b/test-data/unit/check-classes.test @@ -379,7 +379,7 @@ class B(A): [case testOverride__init_subclass__WithDifferentSignature] class A: def __init_subclass__(cls, x: int) -> None: pass -class B(A): +class B(A): # E: Too few arguments for "__init_subclass__" of "A" def __init_subclass__(cls) -> None: pass [case testOverrideWithDecorator] @@ -6164,6 +6164,102 @@ class C(B[int, T]): # TODO: error message could be better. self.x: Tuple[str, T] # E: Incompatible types in assignment (expression has type "Tuple[str, T]", base class "A" defined the type as "Tuple[int, T]") +[case testInitSubclassWrongType] +class Base: + default_name: str + + def __init_subclass__(cls, default_name: str): + super().__init_subclass__() + cls.default_name = default_name + return + +class Child(Base, default_name=5): # E: Argument "default_name" to "__init_subclass__" of "Base" has incompatible type "int"; expected "str" + pass +[builtins fixtures/object_with_init_subclass.pyi] + +[case testInitSubclassTooFewArgs] +class Base: + default_name: str + + def __init_subclass__(cls, default_name: str, **kwargs): + super().__init_subclass__() + cls.default_name = default_name + return + +class Child(Base): # E: Too few arguments for "__init_subclass__" of "Base" + pass +[builtins fixtures/object_with_init_subclass.pyi] + +[case testInitSubclassTooFewArgs2] +class Base: + default_name: str + + def __init_subclass__(cls, default_name: str, thing: int): + super().__init_subclass__() + cls.default_name = default_name + return +# TODO implement this, so that no error is raised? +d = {"default_name": "abc", "thing": 0} +class Child(Base, **d): # E: Too few arguments for "__init_subclass__" of "Base" + pass +[builtins fixtures/object_with_init_subclass.pyi] + +[case testInitSubclassOK] +class Base: + default_name: str + thing: int + + def __init_subclass__(cls, default_name: str, thing:int, **kwargs): + super().__init_subclass__() + cls.default_name = default_name + return + +class Child(Base, thing=5, default_name=""): + pass +[builtins fixtures/object_with_init_subclass.pyi] + +[case testInitSubclassWithMetaclassOK] +class Base(type): + thing: int + + def __init_subclass__(cls, thing: int): + cls.thing = thing + +class Child(Base, metaclass=Base, thing=0): + pass + +[case testTooManyArgsForObject] +class A(thing=5): + pass +[out] +main:1: error: Unexpected keyword argument "thing" for "__init_subclass__" of "object" +tmp/builtins.pyi:5: note: "__init_subclass__" of "object" defined here +[builtins fixtures/object_with_init_subclass.pyi] + +[case testInitSubclassWithImports] +from init_subclass.a import Base +class Child(Base, thing=5): # E: Missing positional arguments "default_name", "kwargs" in call to "__init_subclass__" of "Base" + pass +[file init_subclass/a.py] +class Base: + default_name: str + thing: int + + def __init_subclass__(cls, default_name: str, thing:int, **kwargs): + pass +[file init_subclass/__init__.py] +[builtins fixtures/object_with_init_subclass.pyi] + +[case testInitSubclassWithImportsOK] +from init_subclass.a import MidBase +class Main(MidBase, test=True): pass +[file init_subclass/a.py] +class Base: + def __init_subclass__(cls, **kwargs) -> None: pass +class MidBase(Base): pass +[file init_subclass/__init__.py] +[builtins fixtures/object_with_init_subclass.pyi] + [case testOverrideGenericSelfClassMethod] from typing import Generic, TypeVar, Type, List diff --git a/test-data/unit/fixtures/object_with_init_subclass.pyi b/test-data/unit/fixtures/object_with_init_subclass.pyi new file mode 100644 index 000000000000..407e387bc8c0 --- /dev/null +++ b/test-data/unit/fixtures/object_with_init_subclass.pyi @@ -0,0 +1,52 @@ +from typing import Sequence, Iterator, TypeVar, Mapping, Iterable, Optional, Union, overload, Tuple, Generic + +class object: + def __init__(self) -> None: ... + def __init_subclass__(cls) -> None: ... + +T = TypeVar('T') +KT = TypeVar('KT') +VT = TypeVar('VT') +# copy pased from primitives.pyi +class type: + def __init__(self, x) -> None: pass + +class int: + # Note: this is a simplification of the actual signature + def __init__(self, x: object = ..., base: int = ...) -> None: pass + def __add__(self, i: int) -> int: pass +class float: + def __float__(self) -> float: pass +class complex: pass +class bool(int): pass +class str(Sequence[str]): + def __add__(self, s: str) -> str: pass + def __iter__(self) -> Iterator[str]: pass + def __contains__(self, other: object) -> bool: pass + def __getitem__(self, item: int) -> str: pass + def format(self, *args) -> str: pass +class bytes(Sequence[int]): + def __iter__(self) -> Iterator[int]: pass + def __contains__(self, other: object) -> bool: pass + def __getitem__(self, item: int) -> int: pass +class bytearray: pass +class tuple(Generic[T]): pass +class function: pass +class ellipsis: pass + +# copy-pasted from dict.pyi +class dict(Mapping[KT, VT]): + @overload + def __init__(self, **kwargs: VT) -> None: pass + @overload + def __init__(self, arg: Iterable[Tuple[KT, VT]], **kwargs: VT) -> None: pass + def __getitem__(self, key: KT) -> VT: pass + def __setitem__(self, k: KT, v: VT) -> None: pass + def __iter__(self) -> Iterator[KT]: pass + def __contains__(self, item: object) -> int: pass + def update(self, a: Mapping[KT, VT]) -> None: pass + @overload + def get(self, k: KT) -> Optional[VT]: pass + @overload + def get(self, k: KT, default: Union[KT, T]) -> Union[VT, T]: pass + def __len__(self) -> int: ...
Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.
Alternative Proxies: