Skip to content
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

Support metaclasses #2475

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
26 changes: 26 additions & 0 deletions mypy/nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -1946,6 +1946,27 @@ def __init__(self, names: 'SymbolTable', defn: ClassDef, module_name: str) -> No
for vd in defn.type_vars:
self.type_vars.append(vd.name)

declared_metaclass = None # type: Optional[mypy.types.Instance]
metaclass_type = None # type: Optional[mypy.types.Instance]

def calculate_metaclass_type(self) -> 'Optional[mypy.types.Instance]':
if self.mro is None:
# XXX why does this happen?
Copy link
Member

Choose a reason for hiding this comment

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

Do you have a sample that triggers this condition?

Presumably the clue is the comment in __init__ on line 1940 above. Maybe it has something to do with the MRO being calculated twice -- the first time (in semanal.py on line 850) it may fail if there's an import cycle causing a base class being a forward reference; the second time on line 3128 in ThirdPass, called from build.py line 1440. (There's also a call from fixup.py, but I think that's not what you're seeing.)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Turns out it happens during deserialization in incremental mode: the type of a direct base class may be None. (I should have looked into it long time ago. Sorry)

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 think I will remove the call from deserialize, since it shouldn't be done yet - it should be done from fixup.py. Does it sound reasonable?

return None
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} - {None}
for c in candidates:
if all(other.type in c.type.mro for other in candidates):
return c
Copy link
Member

Choose a reason for hiding this comment

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

How sure are you that the answer does not depend on the order of candidates?

Copy link
Contributor Author

@elazarg elazarg Feb 7, 2017

Choose a reason for hiding this comment

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

Not at all. Perhaps redundant tests checks are better.

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 have replaced the set with a simple list

candidates = [s.declared_metaclass for s in self.mro if s.declared_metaclass is not None]

return None

def is_metaclass(self) -> bool:
return self.has_base('builtins.type')

def name(self) -> str:
"""Short name."""
return self.defn.name
Expand Down Expand Up @@ -2060,6 +2081,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 +2104,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']))
ti.metaclass_type = ti.calculate_metaclass_type()
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):
self.fail("Invalid metaclass '%s'" % defn.metaclass, defn)
return
inst = fill_typevars(sym.node)
assert isinstance(inst, Instance)
Copy link
Member

Choose a reason for hiding this comment

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

I can trigger this assert as follows:

class M(Tuple[int]): pass
class C(metaclass=M): pass

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 think it should be an error then. Do you agree? I don't want to go silently to a fallback,

Copy link
Member

Choose a reason for hiding this comment

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

Yes, you can add the check to line 911.

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
47 changes: 47 additions & 0 deletions test-data/unit/check-classes.test
Original file line number Diff line number Diff line change
Expand Up @@ -2759,3 +2759,50 @@ 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

reveal_type(A.x) # E: Revealed type is 'builtins.int'
Copy link
Member

Choose a reason for hiding this comment

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

This line seems redundant, it's the same as in the previous test.

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 have removed it in the last commit but it still appears here.


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]

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()