diff --git a/mypy/checker.py b/mypy/checker.py index 318a01689827..84e73fc531c9 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -1433,6 +1433,8 @@ def split_around_star(self, items: List[T], star_index: int, return (left, star, right) def type_is_iterable(self, type: Type) -> bool: + if isinstance(type, CallableType) and type.is_type_obj(): + type = type.fallback return (is_subtype(type, self.named_generic_type('typing.Iterable', [AnyType()])) and isinstance(type, Instance)) @@ -2184,6 +2186,10 @@ def visit_call_expr(self, e: CallExpr) -> Type: def visit_yield_from_expr(self, e: YieldFromExpr) -> Type: return self.expr_checker.visit_yield_from_expr(e) + def has_coroutine_decorator(self, t: Type) -> bool: + """Whether t came from a function decorated with `@coroutine`.""" + return isinstance(t, Instance) and t.type.fullname() == 'typing.AwaitableGenerator' + def visit_member_expr(self, e: MemberExpr) -> Type: return self.expr_checker.visit_member_expr(e) @@ -2362,10 +2368,6 @@ def lookup_typeinfo(self, fullname: str) -> TypeInfo: sym = self.lookup_qualified(fullname) return cast(TypeInfo, sym.node) - def type_type(self) -> Instance: - """Return instance type 'type'.""" - return self.named_type('builtins.type') - def object_type(self) -> Instance: """Return instance type 'object'.""" return self.named_type('builtins.object') diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index fa77425db204..90df8698e1b9 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -2099,8 +2099,7 @@ def visit_yield_from_expr(self, e: YieldFromExpr) -> Type: # by __iter__. if isinstance(subexpr_type, AnyType): iter_type = AnyType() - elif (isinstance(subexpr_type, Instance) and - is_subtype(subexpr_type, self.chk.named_type('typing.Iterable'))): + elif self.chk.type_is_iterable(subexpr_type): if is_async_def(subexpr_type) and not has_coroutine_decorator(return_type): self.chk.msg.yield_from_invalid_operand_type(subexpr_type, e) iter_method_type = self.analyze_external_member_access( diff --git a/mypy/checkmember.py b/mypy/checkmember.py index 456ad6b67b17..e3ad6ac35328 100644 --- a/mypy/checkmember.py +++ b/mypy/checkmember.py @@ -184,6 +184,8 @@ def analyze_member_access(name: str, if result: return result fallback = builtin_type('builtins.type') + if item is not None: + fallback = item.type.metaclass_type or fallback return analyze_member_access(name, fallback, node, is_lvalue, is_super, is_operator, builtin_type, not_ready_callback, msg, original_type=original_type, chk=chk) @@ -450,7 +452,7 @@ def type_object_type(info: TypeInfo, builtin_type: Callable[[str], Instance]) -> # Must be an invalid class definition. return AnyType() else: - fallback = builtin_type('builtins.type') + fallback = info.metaclass_type or builtin_type('builtins.type') if init_method.info.fullname() == 'builtins.object': # No non-default __init__ -> look at __new__ instead. new_method = info.get_method('__new__') diff --git a/mypy/constraints.py b/mypy/constraints.py index 533a436448ec..d10b33efbe17 100644 --- a/mypy/constraints.py +++ b/mypy/constraints.py @@ -294,6 +294,8 @@ def visit_type_var(self, template: TypeVarType) -> List[Constraint]: def visit_instance(self, template: Instance) -> List[Constraint]: actual = self.actual res = [] # type: List[Constraint] + if isinstance(actual, CallableType) and actual.fallback is not None: + actual = actual.fallback if isinstance(actual, Instance): instance = actual if (self.direction == SUBTYPE_OF and diff --git a/mypy/nodes.py b/mypy/nodes.py index c9485fe66d56..8d0d15af4c6a 100644 --- a/mypy/nodes.py +++ b/mypy/nodes.py @@ -1876,6 +1876,10 @@ class is generic then it will be a type constructor of higher kind. # Method Resolution Order: the order of looking up attributes. The first # value always to refers to this class. mro = None # type: List[TypeInfo] + + declared_metaclass = None # type: Optional[mypy.types.Instance] + metaclass_type = None # type: mypy.types.Instance + subtypes = None # type: Set[TypeInfo] # Direct subclasses encountered so far names = None # type: SymbolTable # Names defined directly in this type is_abstract = False # Does the class have any abstract attributes? @@ -2005,6 +2009,21 @@ def calculate_mro(self) -> None: self.mro = mro self.is_enum = self._calculate_is_enum() + def calculate_metaclass_type(self) -> 'Optional[mypy.types.Instance]': + declared = self.declared_metaclass + if declared is not None and not declared.type.has_base('builtins.type'): + return declared + if self._fullname == 'builtins.type': + return mypy.types.Instance(self, []) + candidates = [s.declared_metaclass for s in self.mro if s.declared_metaclass is not None] + for c in candidates: + if all(other.type in c.type.mro for other in candidates): + return c + return None + + def is_metaclass(self) -> bool: + return self.has_base('builtins.type') + def _calculate_is_enum(self) -> bool: """ If this is "enum.Enum" itself, then yes, it's an enum. @@ -2060,6 +2079,8 @@ def serialize(self) -> JsonDict: 'type_vars': self.type_vars, 'bases': [b.serialize() for b in self.bases], '_promote': None if self._promote is None else self._promote.serialize(), + 'declared_metaclass': (None if self.declared_metaclass is None + else self.declared_metaclass.serialize()), 'tuple_type': None if self.tuple_type is None else self.tuple_type.serialize(), 'typeddict_type': None if self.typeddict_type is None else self.typeddict_type.serialize(), @@ -2081,6 +2102,9 @@ def deserialize(cls, data: JsonDict) -> 'TypeInfo': ti.bases = [mypy.types.Instance.deserialize(b) for b in data['bases']] ti._promote = (None if data['_promote'] is None else mypy.types.Type.deserialize(data['_promote'])) + ti.declared_metaclass = (None if data['declared_metaclass'] is None + else mypy.types.Instance.deserialize(data['declared_metaclass'])) + # NOTE: ti.metaclass_type and ti.mro will be set in the fixup phase. ti.tuple_type = (None if data['tuple_type'] is None else mypy.types.TupleType.deserialize(data['tuple_type'])) ti.typeddict_type = (None if data['typeddict_type'] is None diff --git a/mypy/semanal.py b/mypy/semanal.py index 71a8323be292..66553325ece2 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -905,8 +905,20 @@ def analyze_metaclass(self, defn: ClassDef) -> None: self.fail("Dynamic metaclass not supported for '%s'" % defn.name, defn) return sym = self.lookup_qualified(defn.metaclass, defn) - if sym is not None and not isinstance(sym.node, TypeInfo): + if sym is None: + # Probably a name error - it is already handled elsewhere + return + if not isinstance(sym.node, TypeInfo) or sym.node.tuple_type is not None: self.fail("Invalid metaclass '%s'" % defn.metaclass, defn) + return + inst = fill_typevars(sym.node) + assert isinstance(inst, Instance) + defn.info.declared_metaclass = inst + defn.info.metaclass_type = defn.info.calculate_metaclass_type() + if defn.info.metaclass_type is None: + # Inconsistency may happen due to multiple baseclasses even in classes that + # do not declare explicit metaclass, but it's harder to catch at this stage + self.fail("Inconsistent metaclass structure for '%s'" % defn.name, defn) def object_type(self) -> Instance: return self.named_type('__builtins__.object') diff --git a/mypy/types.py b/mypy/types.py index 3d27d52d8a87..4275403f5844 100644 --- a/mypy/types.py +++ b/mypy/types.py @@ -643,7 +643,8 @@ def copy_modified(self, ) def is_type_obj(self) -> bool: - return self.fallback.type is not None and self.fallback.type.fullname() == 'builtins.type' + t = self.fallback.type + return t is not None and t.is_metaclass() def is_concrete_type_obj(self) -> bool: return self.is_type_obj() and self.is_classmethod_class diff --git a/test-data/unit/check-classes.test b/test-data/unit/check-classes.test index 9e46ef98e8ad..af69bf7c4c30 100644 --- a/test-data/unit/check-classes.test +++ b/test-data/unit/check-classes.test @@ -2759,3 +2759,55 @@ class B(A): class C(B): x = '' [out] + +[case testInvalidMetaclassStructure] +class X(type): pass +class Y(type): pass +class A(metaclass=X): pass +class B(A, metaclass=Y): pass # E: Inconsistent metaclass structure for 'B' + +[case testMetaclassNoTypeReveal] +class M: + x = 0 # type: int + +class A(metaclass=M): pass + +reveal_type(A.x) # E: Revealed type is 'builtins.int' + +[case testMetaclassTypeReveal] +from typing import Type +class M(type): + x = 0 # type: int + +class A(metaclass=M): pass + +def f(TA: Type[A]): + reveal_type(TA) # E: Revealed type is 'Type[__main__.A]' + reveal_type(TA.x) # E: Revealed type is 'builtins.int' + +[case testMetaclassIterable] +from typing import Iterable, Iterator + +class BadMeta(type): + def __iter__(self) -> Iterator[int]: yield 1 + +class Bad(metaclass=BadMeta): pass + +for _ in Bad: pass # E: Iterable expected + +class GoodMeta(type, Iterable[int]): + def __iter__(self) -> Iterator[int]: yield 1 + +class Good(metaclass=GoodMeta): pass +for _ in Good: pass +reveal_type(list(Good)) # E: Revealed type is 'builtins.list[builtins.int*]' + +[builtins fixtures/list.pyi] + +[case testMetaclassTuple] +from typing import Tuple + +class M(Tuple[int]): pass +class C(metaclass=M): pass # E: Invalid metaclass 'M' + +[builtins fixtures/tuple.pyi] \ No newline at end of file diff --git a/test-data/unit/lib-stub/abc.pyi b/test-data/unit/lib-stub/abc.pyi index 4afe734d29c1..9208f42e9635 100644 --- a/test-data/unit/lib-stub/abc.pyi +++ b/test-data/unit/lib-stub/abc.pyi @@ -1,3 +1,3 @@ -class ABCMeta: pass +class ABCMeta(type): pass abstractmethod = object() abstractproperty = object()