diff --git a/mypy/semanal.py b/mypy/semanal.py index 95a7bf6e3630..a29da6aabdac 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -758,8 +758,12 @@ def clean_up_bases_and_infer_type_variables(self, defn: ClassDef) -> None: Now we will remove Generic[T] from bases of Foo and infer that the type variable 'T' is a type argument of Foo. + We also process six.with_metaclass() here. + Note that this is performed *before* semantic analysis. """ + # First process six.with_metaclass if present and well-formed + defn.base_type_exprs, defn.metaclass = self.check_with_metaclass(defn) removed = [] # type: List[int] declared_tvars = [] # type: TypeVarList for i, base_expr in enumerate(defn.base_type_exprs): @@ -938,6 +942,7 @@ def analyze_base_classes(self, defn: ClassDef) -> None: base_types = [] # type: List[Instance] info = defn.info + for base_expr in defn.base_type_exprs: try: base = self.expr_to_analyzed_type(base_expr) @@ -989,6 +994,27 @@ def analyze_base_classes(self, defn: ClassDef) -> None: if defn.info.is_enum and defn.type_vars: self.fail("Enum class cannot be generic", defn) + def check_with_metaclass(self, defn: ClassDef) -> Tuple[List[Expression], Optional[str]]: + # Special-case six.with_metaclass(M, B1, B2, ...). + base_type_exprs, metaclass = defn.base_type_exprs, defn.metaclass + if metaclass is None and len(base_type_exprs) == 1: + base_expr = base_type_exprs[0] + if isinstance(base_expr, CallExpr) and isinstance(base_expr.callee, RefExpr): + base_expr.callee.accept(self) + if (base_expr.callee.fullname == 'six.with_metaclass' + and len(base_expr.args) >= 1 + and all(kind == ARG_POS for kind in base_expr.arg_kinds)): + metaclass_expr = base_expr.args[0] + if isinstance(metaclass_expr, NameExpr): + metaclass = metaclass_expr.name + elif isinstance(metaclass_expr, MemberExpr): + metaclass = get_member_expr_fullname(metaclass_expr) + else: + self.fail("Dynamic metaclass not supported for '%s'" % defn.name, + metaclass_expr) + return (base_expr.args[1:], metaclass) + return (base_type_exprs, metaclass) + def expr_to_analyzed_type(self, expr: Expression) -> Type: if isinstance(expr, CallExpr): expr.accept(self) diff --git a/test-data/unit/check-classes.test b/test-data/unit/check-classes.test index 1dd3353ec903..d6f011719d53 100644 --- a/test-data/unit/check-classes.test +++ b/test-data/unit/check-classes.test @@ -3137,3 +3137,94 @@ class M(type): class A(metaclass=M): pass reveal_type(type(A).x) # E: Revealed type is 'builtins.int' +[case testSixWithMetaclass] +import six +class M(type): + x = 5 +class A(six.with_metaclass(M)): pass +reveal_type(type(A).x) # E: Revealed type is 'builtins.int' + +[case testSixWithMetaclass_python2] +import six +class M(type): + x = 5 +class A(six.with_metaclass(M)): pass +reveal_type(type(A).x) # E: Revealed type is 'builtins.int' + +[case testFromSixWithMetaclass] +from six import with_metaclass +class M(type): + x = 5 +class A(with_metaclass(M)): pass +reveal_type(type(A).x) # E: Revealed type is 'builtins.int' + +[case testSixWithMetaclassImportFrom] +import six +from metadefs import M +class A(six.with_metaclass(M)): pass +reveal_type(type(A).x) # E: Revealed type is 'builtins.int' +[file metadefs.py] +class M(type): + x = 5 + +[case testSixWithMetaclassImport] +import six +import metadefs +class A(six.with_metaclass(metadefs.M)): pass +reveal_type(type(A).x) # E: Revealed type is 'builtins.int' +[file metadefs.py] +class M(type): + x = 5 + +[case testSixWithMetaclassAndBase] +import six +class M(type): + x = 5 +class A: + def foo(self): pass +class B: + def bar(self): pass +class C1(six.with_metaclass(M, A)): pass +class C2(six.with_metaclass(M, A, B)): pass +reveal_type(type(C1).x) # E: Revealed type is 'builtins.int' +reveal_type(type(C2).x) # E: Revealed type is 'builtins.int' +C1().foo() +C1().bar() # E: "C1" has no attribute "bar" +C2().foo() +C2().bar() +C2().baz() # E: "C2" has no attribute "baz" + +[case testSixWithMetaclassGenerics] +from typing import Generic, GenericMeta, TypeVar +import six +class DestroyableMeta(type): + pass +class Destroyable(six.with_metaclass(DestroyableMeta)): + pass +T_co = TypeVar('T_co', bound='Destroyable', covariant=True) +class ArcMeta(GenericMeta, DestroyableMeta): + pass +class Arc(six.with_metaclass(ArcMeta, Generic[T_co], Destroyable)): + pass +class MyDestr(Destroyable): + pass +reveal_type(Arc[MyDestr]()) # E: Revealed type is '__main__.Arc[__main__.MyDestr*]' +[builtins fixtures/bool.pyi] + +[case testSixWithMetaclassErrors] +import six +class M(type): pass +class A(object): pass +def f() -> type: return M +class C1(six.with_metaclass(M), object): pass # E: Invalid base class +class C2(C1, six.with_metaclass(M)): pass # E: Invalid base class +class C3(six.with_metaclass(A)): pass # E: Metaclasses not inheriting from 'type' are not supported +class C4(six.with_metaclass(M), metaclass=M): pass # E: Invalid base class +class C5(six.with_metaclass(f())): pass # E: Dynamic metaclass not supported for 'C5' + +[case testSixWithMetaclassErrors_python2-skip] +# No error here yet +import six +class M(type): pass +class C4(six.with_metaclass(M)): + __metaclass__ = M diff --git a/test-data/unit/lib-stub/six.pyi b/test-data/unit/lib-stub/six.pyi new file mode 100644 index 000000000000..a6faa32988cb --- /dev/null +++ b/test-data/unit/lib-stub/six.pyi @@ -0,0 +1,2 @@ +from typing import Type +def with_metaclass(mcls: Type[type], *args: type) -> type: pass diff --git a/test-data/unit/lib-stub/typing.pyi b/test-data/unit/lib-stub/typing.pyi index 01ac7b14f7b9..51ffcca74fac 100644 --- a/test-data/unit/lib-stub/typing.pyi +++ b/test-data/unit/lib-stub/typing.pyi @@ -3,6 +3,8 @@ from abc import abstractmethod +class GenericMeta(type): pass + cast = 0 overload = 0 Any = 0