Skip to content

Support metaclasses #2475

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 18 commits into from
Feb 7, 2017
10 changes: 6 additions & 4 deletions mypy/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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')
Expand Down
3 changes: 1 addition & 2 deletions mypy/checkexpr.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
4 changes: 3 additions & 1 deletion mypy/checkmember.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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__')
Expand Down
2 changes: 2 additions & 0 deletions mypy/constraints.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
24 changes: 24 additions & 0 deletions mypy/nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -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?
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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(),
Expand All @@ -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
Expand Down
14 changes: 13 additions & 1 deletion mypy/semanal.py
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down
3 changes: 2 additions & 1 deletion mypy/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
52 changes: 52 additions & 0 deletions test-data/unit/check-classes.test
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this works, shouldn't list(C) work too?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just assumed it will work. Turns out it didn't, but after handling the fallback in the solver it does.

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]
2 changes: 1 addition & 1 deletion test-data/unit/lib-stub/abc.pyi
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
class ABCMeta: pass
class ABCMeta(type): pass
abstractmethod = object()
abstractproperty = object()