Skip to content

Commit

Permalink
Detect metaclass conflicts (#13598)
Browse files Browse the repository at this point in the history
Recreate of #13565
Closes #13563
  • Loading branch information
sobolevn authored Sep 4, 2022
1 parent 130e1a4 commit 9d21615
Show file tree
Hide file tree
Showing 5 changed files with 79 additions and 21 deletions.
9 changes: 6 additions & 3 deletions docs/source/metaclasses.rst
Original file line number Diff line number Diff line change
Expand Up @@ -72,12 +72,15 @@ so it's better not to combine metaclasses and class hierarchies:
class A1(metaclass=M1): pass
class A2(metaclass=M2): pass
class B1(A1, metaclass=M2): pass # Mypy Error: Inconsistent metaclass structure for "B1"
class B1(A1, metaclass=M2): pass # Mypy Error: metaclass conflict
# At runtime the above definition raises an exception
# TypeError: metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases
# Same runtime error as in B1, but mypy does not catch it yet
class B12(A1, A2): pass
class B12(A1, A2): pass # Mypy Error: metaclass conflict
# This can be solved via a common metaclass subtype:
class CorrectMeta(M1, M2): pass
class B2(A1, A2, metaclass=CorrectMeta): pass # OK, runtime is also OK
* Mypy does not understand dynamically-computed metaclasses,
such as ``class A(metaclass=f()): ...``
Expand Down
30 changes: 30 additions & 0 deletions mypy/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -2044,6 +2044,7 @@ def visit_class_def(self, defn: ClassDef) -> None:
if not defn.has_incompatible_baseclass:
# Otherwise we've already found errors; more errors are not useful
self.check_multiple_inheritance(typ)
self.check_metaclass_compatibility(typ)
self.check_final_deletable(typ)

if defn.decorators:
Expand Down Expand Up @@ -2383,6 +2384,35 @@ class C(B, A[int]): ... # this is unsafe because...
if not ok:
self.msg.base_class_definitions_incompatible(name, base1, base2, ctx)

def check_metaclass_compatibility(self, typ: TypeInfo) -> None:
"""Ensures that metaclasses of all parent types are compatible."""
if (
typ.is_metaclass()
or typ.is_protocol
or typ.is_named_tuple
or typ.is_enum
or typ.typeddict_type is not None
):
return # Reasonable exceptions from this check

metaclasses = [
entry.metaclass_type
for entry in typ.mro[1:-1]
if entry.metaclass_type
and not is_named_instance(entry.metaclass_type, "builtins.type")
]
if not metaclasses:
return
if typ.metaclass_type is not None and all(
is_subtype(typ.metaclass_type, meta) for meta in metaclasses
):
return
self.fail(
"Metaclass conflict: the metaclass of a derived class must be "
"a (non-strict) subclass of the metaclasses of all its bases",
typ,
)

def visit_import_from(self, node: ImportFrom) -> None:
self.check_import(node)

Expand Down
16 changes: 4 additions & 12 deletions mypy/semanal.py
Original file line number Diff line number Diff line change
Expand Up @@ -2120,18 +2120,10 @@ def recalculate_metaclass(self, defn: ClassDef, declared_metaclass: Instance | N
abc_meta = self.named_type_or_none("abc.ABCMeta", [])
if abc_meta is not None: # May be None in tests with incomplete lib-stub.
defn.info.metaclass_type = abc_meta
if declared_metaclass is not None and 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
if defn.metaclass is not None:
self.fail(f'Inconsistent metaclass structure for "{defn.name}"', defn)
else:
if defn.info.metaclass_type and defn.info.metaclass_type.type.has_base(
"enum.EnumMeta"
):
defn.info.is_enum = True
if defn.type_vars:
self.fail("Enum class cannot be generic", defn)
if defn.info.metaclass_type and defn.info.metaclass_type.type.has_base("enum.EnumMeta"):
defn.info.is_enum = True
if defn.type_vars:
self.fail("Enum class cannot be generic", defn)

#
# Imports
Expand Down
41 changes: 37 additions & 4 deletions test-data/unit/check-classes.test
Original file line number Diff line number Diff line change
Expand Up @@ -4351,7 +4351,7 @@ class C(B):
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"
class B(A, metaclass=Y): pass # E: Metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases

[case testMetaclassNoTypeReveal]
class M:
Expand Down Expand Up @@ -5213,8 +5213,8 @@ class CD(six.with_metaclass(M)): pass # E: Multiple metaclass definitions
class M1(type): pass
class Q1(metaclass=M1): pass
@six.add_metaclass(M)
class CQA(Q1): pass # E: Inconsistent metaclass structure for "CQA"
class CQW(six.with_metaclass(M, Q1)): pass # E: Inconsistent metaclass structure for "CQW"
class CQA(Q1): pass # E: Metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases
class CQW(six.with_metaclass(M, Q1)): pass # E: Metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases
[builtins fixtures/tuple.pyi]

[case testSixMetaclassAny]
Expand Down Expand Up @@ -5319,7 +5319,7 @@ class C5(future.utils.with_metaclass(f())): pass # E: Dynamic metaclass not sup

class M1(type): pass
class Q1(metaclass=M1): pass
class CQW(future.utils.with_metaclass(M, Q1)): pass # E: Inconsistent metaclass structure for "CQW"
class CQW(future.utils.with_metaclass(M, Q1)): pass # E: Metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases
[builtins fixtures/tuple.pyi]

[case testFutureMetaclassAny]
Expand Down Expand Up @@ -6718,6 +6718,39 @@ class Meta(A): pass
from m import Meta
class A(metaclass=Meta): pass

[case testMetaclassConflict]
class MyMeta1(type): ...
class MyMeta2(type): ...
class MyMeta3(type): ...
class A(metaclass=MyMeta1): ...
class B(metaclass=MyMeta2): ...
class C(metaclass=type): ...
class A1(A): ...
class E: ...

class CorrectMeta(MyMeta1, MyMeta2): ...
class CorrectSubclass1(A1, B, E, metaclass=CorrectMeta): ...
class CorrectSubclass2(A, B, E, metaclass=CorrectMeta): ...
class CorrectSubclass3(B, A, metaclass=CorrectMeta): ...

class ChildOfCorrectSubclass1(CorrectSubclass1): ...

class CorrectWithType1(C, A1): ...
class CorrectWithType2(B, C): ...

class Conflict1(A1, B, E): ... # E: Metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases
class Conflict2(A, B): ... # E: Metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases
class Conflict3(B, A): ... # E: Metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases

class ChildOfConflict1(Conflict3): ... # E: Metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases
class ChildOfConflict2(Conflict3, metaclass=CorrectMeta): ...

class ConflictingMeta(MyMeta1, MyMeta3): ...
class Conflict4(A1, B, E, metaclass=ConflictingMeta): ... # E: Metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases

class ChildOfCorrectButWrongMeta(CorrectSubclass1, metaclass=ConflictingMeta): # E: Metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases
...

[case testGenericOverride]
from typing import Generic, TypeVar, Any

Expand Down
4 changes: 2 additions & 2 deletions test-data/unit/fine-grained.test
Original file line number Diff line number Diff line change
Expand Up @@ -2968,7 +2968,7 @@ class M(type):
pass
[out]
==
a.py:3: error: Inconsistent metaclass structure for "D"
a.py:3: error: Metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases

[case testFineMetaclassDeclaredUpdate]
import a
Expand All @@ -2984,7 +2984,7 @@ class M(type): pass
class M2(type): pass
[out]
==
a.py:3: error: Inconsistent metaclass structure for "D"
a.py:3: error: Metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases

[case testFineMetaclassRemoveFromClass]
import a
Expand Down

0 comments on commit 9d21615

Please sign in to comment.