Skip to content

Commit cab1e0d

Browse files
elazarggvanrossum
authored andcommitted
Support metaclasses (#2475)
1 parent c21ede7 commit cab1e0d

File tree

9 files changed

+104
-10
lines changed

9 files changed

+104
-10
lines changed

mypy/checker.py

+6-4
Original file line numberDiff line numberDiff line change
@@ -1433,6 +1433,8 @@ def split_around_star(self, items: List[T], star_index: int,
14331433
return (left, star, right)
14341434

14351435
def type_is_iterable(self, type: Type) -> bool:
1436+
if isinstance(type, CallableType) and type.is_type_obj():
1437+
type = type.fallback
14361438
return (is_subtype(type, self.named_generic_type('typing.Iterable',
14371439
[AnyType()])) and
14381440
isinstance(type, Instance))
@@ -2184,6 +2186,10 @@ def visit_call_expr(self, e: CallExpr) -> Type:
21842186
def visit_yield_from_expr(self, e: YieldFromExpr) -> Type:
21852187
return self.expr_checker.visit_yield_from_expr(e)
21862188

2189+
def has_coroutine_decorator(self, t: Type) -> bool:
2190+
"""Whether t came from a function decorated with `@coroutine`."""
2191+
return isinstance(t, Instance) and t.type.fullname() == 'typing.AwaitableGenerator'
2192+
21872193
def visit_member_expr(self, e: MemberExpr) -> Type:
21882194
return self.expr_checker.visit_member_expr(e)
21892195

@@ -2362,10 +2368,6 @@ def lookup_typeinfo(self, fullname: str) -> TypeInfo:
23622368
sym = self.lookup_qualified(fullname)
23632369
return cast(TypeInfo, sym.node)
23642370

2365-
def type_type(self) -> Instance:
2366-
"""Return instance type 'type'."""
2367-
return self.named_type('builtins.type')
2368-
23692371
def object_type(self) -> Instance:
23702372
"""Return instance type 'object'."""
23712373
return self.named_type('builtins.object')

mypy/checkexpr.py

+1-2
Original file line numberDiff line numberDiff line change
@@ -2099,8 +2099,7 @@ def visit_yield_from_expr(self, e: YieldFromExpr) -> Type:
20992099
# by __iter__.
21002100
if isinstance(subexpr_type, AnyType):
21012101
iter_type = AnyType()
2102-
elif (isinstance(subexpr_type, Instance) and
2103-
is_subtype(subexpr_type, self.chk.named_type('typing.Iterable'))):
2102+
elif self.chk.type_is_iterable(subexpr_type):
21042103
if is_async_def(subexpr_type) and not has_coroutine_decorator(return_type):
21052104
self.chk.msg.yield_from_invalid_operand_type(subexpr_type, e)
21062105
iter_method_type = self.analyze_external_member_access(

mypy/checkmember.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,8 @@ def analyze_member_access(name: str,
184184
if result:
185185
return result
186186
fallback = builtin_type('builtins.type')
187+
if item is not None:
188+
fallback = item.type.metaclass_type or fallback
187189
return analyze_member_access(name, fallback, node, is_lvalue, is_super,
188190
is_operator, builtin_type, not_ready_callback, msg,
189191
original_type=original_type, chk=chk)
@@ -450,7 +452,7 @@ def type_object_type(info: TypeInfo, builtin_type: Callable[[str], Instance]) ->
450452
# Must be an invalid class definition.
451453
return AnyType()
452454
else:
453-
fallback = builtin_type('builtins.type')
455+
fallback = info.metaclass_type or builtin_type('builtins.type')
454456
if init_method.info.fullname() == 'builtins.object':
455457
# No non-default __init__ -> look at __new__ instead.
456458
new_method = info.get_method('__new__')

mypy/constraints.py

+2
Original file line numberDiff line numberDiff line change
@@ -294,6 +294,8 @@ def visit_type_var(self, template: TypeVarType) -> List[Constraint]:
294294
def visit_instance(self, template: Instance) -> List[Constraint]:
295295
actual = self.actual
296296
res = [] # type: List[Constraint]
297+
if isinstance(actual, CallableType) and actual.fallback is not None:
298+
actual = actual.fallback
297299
if isinstance(actual, Instance):
298300
instance = actual
299301
if (self.direction == SUBTYPE_OF and

mypy/nodes.py

+24
Original file line numberDiff line numberDiff line change
@@ -1876,6 +1876,10 @@ class is generic then it will be a type constructor of higher kind.
18761876
# Method Resolution Order: the order of looking up attributes. The first
18771877
# value always to refers to this class.
18781878
mro = None # type: List[TypeInfo]
1879+
1880+
declared_metaclass = None # type: Optional[mypy.types.Instance]
1881+
metaclass_type = None # type: mypy.types.Instance
1882+
18791883
subtypes = None # type: Set[TypeInfo] # Direct subclasses encountered so far
18801884
names = None # type: SymbolTable # Names defined directly in this type
18811885
is_abstract = False # Does the class have any abstract attributes?
@@ -2005,6 +2009,21 @@ def calculate_mro(self) -> None:
20052009
self.mro = mro
20062010
self.is_enum = self._calculate_is_enum()
20072011

2012+
def calculate_metaclass_type(self) -> 'Optional[mypy.types.Instance]':
2013+
declared = self.declared_metaclass
2014+
if declared is not None and not declared.type.has_base('builtins.type'):
2015+
return declared
2016+
if self._fullname == 'builtins.type':
2017+
return mypy.types.Instance(self, [])
2018+
candidates = [s.declared_metaclass for s in self.mro if s.declared_metaclass is not None]
2019+
for c in candidates:
2020+
if all(other.type in c.type.mro for other in candidates):
2021+
return c
2022+
return None
2023+
2024+
def is_metaclass(self) -> bool:
2025+
return self.has_base('builtins.type')
2026+
20082027
def _calculate_is_enum(self) -> bool:
20092028
"""
20102029
If this is "enum.Enum" itself, then yes, it's an enum.
@@ -2060,6 +2079,8 @@ def serialize(self) -> JsonDict:
20602079
'type_vars': self.type_vars,
20612080
'bases': [b.serialize() for b in self.bases],
20622081
'_promote': None if self._promote is None else self._promote.serialize(),
2082+
'declared_metaclass': (None if self.declared_metaclass is None
2083+
else self.declared_metaclass.serialize()),
20632084
'tuple_type': None if self.tuple_type is None else self.tuple_type.serialize(),
20642085
'typeddict_type':
20652086
None if self.typeddict_type is None else self.typeddict_type.serialize(),
@@ -2081,6 +2102,9 @@ def deserialize(cls, data: JsonDict) -> 'TypeInfo':
20812102
ti.bases = [mypy.types.Instance.deserialize(b) for b in data['bases']]
20822103
ti._promote = (None if data['_promote'] is None
20832104
else mypy.types.Type.deserialize(data['_promote']))
2105+
ti.declared_metaclass = (None if data['declared_metaclass'] is None
2106+
else mypy.types.Instance.deserialize(data['declared_metaclass']))
2107+
# NOTE: ti.metaclass_type and ti.mro will be set in the fixup phase.
20842108
ti.tuple_type = (None if data['tuple_type'] is None
20852109
else mypy.types.TupleType.deserialize(data['tuple_type']))
20862110
ti.typeddict_type = (None if data['typeddict_type'] is None

mypy/semanal.py

+13-1
Original file line numberDiff line numberDiff line change
@@ -905,8 +905,20 @@ def analyze_metaclass(self, defn: ClassDef) -> None:
905905
self.fail("Dynamic metaclass not supported for '%s'" % defn.name, defn)
906906
return
907907
sym = self.lookup_qualified(defn.metaclass, defn)
908-
if sym is not None and not isinstance(sym.node, TypeInfo):
908+
if sym is None:
909+
# Probably a name error - it is already handled elsewhere
910+
return
911+
if not isinstance(sym.node, TypeInfo) or sym.node.tuple_type is not None:
909912
self.fail("Invalid metaclass '%s'" % defn.metaclass, defn)
913+
return
914+
inst = fill_typevars(sym.node)
915+
assert isinstance(inst, Instance)
916+
defn.info.declared_metaclass = inst
917+
defn.info.metaclass_type = defn.info.calculate_metaclass_type()
918+
if defn.info.metaclass_type is None:
919+
# Inconsistency may happen due to multiple baseclasses even in classes that
920+
# do not declare explicit metaclass, but it's harder to catch at this stage
921+
self.fail("Inconsistent metaclass structure for '%s'" % defn.name, defn)
910922

911923
def object_type(self) -> Instance:
912924
return self.named_type('__builtins__.object')

mypy/types.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -643,7 +643,8 @@ def copy_modified(self,
643643
)
644644

645645
def is_type_obj(self) -> bool:
646-
return self.fallback.type is not None and self.fallback.type.fullname() == 'builtins.type'
646+
t = self.fallback.type
647+
return t is not None and t.is_metaclass()
647648

648649
def is_concrete_type_obj(self) -> bool:
649650
return self.is_type_obj() and self.is_classmethod_class

test-data/unit/check-classes.test

+52
Original file line numberDiff line numberDiff line change
@@ -2759,3 +2759,55 @@ class B(A):
27592759
class C(B):
27602760
x = ''
27612761
[out]
2762+
2763+
[case testInvalidMetaclassStructure]
2764+
class X(type): pass
2765+
class Y(type): pass
2766+
class A(metaclass=X): pass
2767+
class B(A, metaclass=Y): pass # E: Inconsistent metaclass structure for 'B'
2768+
2769+
[case testMetaclassNoTypeReveal]
2770+
class M:
2771+
x = 0 # type: int
2772+
2773+
class A(metaclass=M): pass
2774+
2775+
reveal_type(A.x) # E: Revealed type is 'builtins.int'
2776+
2777+
[case testMetaclassTypeReveal]
2778+
from typing import Type
2779+
class M(type):
2780+
x = 0 # type: int
2781+
2782+
class A(metaclass=M): pass
2783+
2784+
def f(TA: Type[A]):
2785+
reveal_type(TA) # E: Revealed type is 'Type[__main__.A]'
2786+
reveal_type(TA.x) # E: Revealed type is 'builtins.int'
2787+
2788+
[case testMetaclassIterable]
2789+
from typing import Iterable, Iterator
2790+
2791+
class BadMeta(type):
2792+
def __iter__(self) -> Iterator[int]: yield 1
2793+
2794+
class Bad(metaclass=BadMeta): pass
2795+
2796+
for _ in Bad: pass # E: Iterable expected
2797+
2798+
class GoodMeta(type, Iterable[int]):
2799+
def __iter__(self) -> Iterator[int]: yield 1
2800+
2801+
class Good(metaclass=GoodMeta): pass
2802+
for _ in Good: pass
2803+
reveal_type(list(Good)) # E: Revealed type is 'builtins.list[builtins.int*]'
2804+
2805+
[builtins fixtures/list.pyi]
2806+
2807+
[case testMetaclassTuple]
2808+
from typing import Tuple
2809+
2810+
class M(Tuple[int]): pass
2811+
class C(metaclass=M): pass # E: Invalid metaclass 'M'
2812+
2813+
[builtins fixtures/tuple.pyi]

test-data/unit/lib-stub/abc.pyi

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
class ABCMeta: pass
1+
class ABCMeta(type): pass
22
abstractmethod = object()
33
abstractproperty = object()

0 commit comments

Comments
 (0)