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

Metaclass conflict check improvements #17682

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions docs/source/error_code_list.rst
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,35 @@ You can use :py:data:`~typing.Callable` as the type for callable objects:
for x in objs:
f(x)

.. _code-metaclass:

Check the validity of a class's metaclass [metaclass]
-----------------------------------------------------

Mypy checks whether the metaclass of a class is valid. The metaclass
must be a subclass of ``type``. Further, the class hierarchy must yield
a consistent metaclass. For more details, see the
`Python documentation <https://docs.python.org/3.13/reference/datamodel.html#determining-the-appropriate-metaclass>`_

Note that mypy's metaclass checking is limited and may produce false-positives.
See also :ref:`limitations`.

Example with an error:

.. code-block:: python

class GoodMeta(type):
pass

class BadMeta:
pass

class A1(metaclass=GoodMeta): # OK
pass

class A2(metaclass=BadMeta): # Error: Metaclasses not inheriting from "type" are not supported [metaclass]
pass

.. _code-var-annotated:

Require annotation if variable type is unclear [var-annotated]
Expand Down
31 changes: 31 additions & 0 deletions docs/source/metaclasses.rst
Original file line number Diff line number Diff line change
Expand Up @@ -86,3 +86,34 @@ so it's better not to combine metaclasses and class hierarchies:
such as ``class A(metaclass=f()): ...``
* Mypy does not and cannot understand arbitrary metaclass code.
* Mypy only recognizes subclasses of :py:class:`type` as potential metaclasses.

For some builtin types, mypy assumes that their metaclass is :py:class:`abc.ABCMeta`
even if it's :py:class:`type`. In those cases, you can either

* use :py:class:`abc.ABCMeta` instead of :py:class:`type` as the
superclass of your metaclass if that works in your use case,
* mute the error with ``# type: ignore[metaclass]``, or
* compute the metaclass' superclass dynamically, which mypy doesn't understand
so it will also need to be muted.

.. code-block:: python

import abc

assert type(tuple) is type # metaclass of tuple is type

# the problem:
class M0(type): pass
class A0(tuple, metaclass=M1): pass # Mypy Error: metaclass conflict

# option 1: use ABCMeta instead of type
class M1(abc.ABCMeta): pass
class A1(tuple, metaclass=M1): pass

# option 2: mute the error
class M2(type): pass
class A2(tuple, metaclass=M2): pass # type: ignore[metaclass]

# option 3: compute the metaclass dynamically
class M3(type(tuple)): pass # type: ignore[metaclass]
class A3(tuple, metaclass=M3): pass
21 changes: 19 additions & 2 deletions mypy/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -2831,10 +2831,27 @@ def check_metaclass_compatibility(self, typ: TypeInfo) -> None:
is_subtype(typ.metaclass_type, meta) for meta in metaclasses
):
return
if typ.declared_metaclass is None:
metaclass_names = { # using a dict as ordered set
str(meta): None for meta in metaclasses
}.keys()
conflict_info = f"found metaclasses of bases: {', '.join(metaclass_names)}"
else:
uncovered_metaclass_names = { # using a dict as ordered set
str(meta): None
for meta in metaclasses
if not is_subtype(typ.declared_metaclass, meta)
}.keys()
conflict_info = (
f"own metaclass {typ.declared_metaclass} is not a subclass of "
f"{', '.join(uncovered_metaclass_names)}"
)
self.fail(
"Metaclass conflict: the metaclass of a derived class must be "
"a (non-strict) subclass of the metaclasses of all its bases",
"Metaclass conflict: the metaclass of a derived class must be a "
"(non-strict) subclass of the metaclasses of all its bases - "
f"{conflict_info}",
typ,
code=codes.METACLASS,
)

def visit_import_from(self, node: ImportFrom) -> None:
Expand Down
1 change: 1 addition & 0 deletions mypy/errorcodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,7 @@ def __hash__(self) -> int:
"General",
default_enabled=False,
)
METACLASS: Final[ErrorCode] = ErrorCode("metaclass", "Ensure that metaclass is valid", "General")

# Syntax errors are often blocking.
SYNTAX: Final[ErrorCode] = ErrorCode("syntax", "Report syntax errors", "General")
Expand Down
17 changes: 13 additions & 4 deletions mypy/semanal.py
Original file line number Diff line number Diff line change
Expand Up @@ -2592,7 +2592,7 @@ def infer_metaclass_and_bases_from_compat_helpers(self, defn: ClassDef) -> None:
if len(metas) == 0:
return
if len(metas) > 1:
self.fail("Multiple metaclass definitions", defn)
self.fail("Multiple metaclass definitions", defn, code=codes.METACLASS)
return
defn.metaclass = metas.pop()

Expand Down Expand Up @@ -2648,7 +2648,11 @@ def get_declared_metaclass(
elif isinstance(metaclass_expr, MemberExpr):
metaclass_name = get_member_expr_fullname(metaclass_expr)
if metaclass_name is None:
self.fail(f'Dynamic metaclass not supported for "{name}"', metaclass_expr)
self.fail(
f'Dynamic metaclass not supported for "{name}"',
metaclass_expr,
code=codes.METACLASS,
)
return None, False, True
sym = self.lookup_qualified(metaclass_name, metaclass_expr)
if sym is None:
Expand All @@ -2659,6 +2663,7 @@ def get_declared_metaclass(
self.fail(
f'Class cannot use "{sym.node.name}" as a metaclass (has type "Any")',
metaclass_expr,
code=codes.METACLASS,
)
return None, False, True
if isinstance(sym.node, PlaceholderNode):
Expand All @@ -2676,11 +2681,15 @@ def get_declared_metaclass(
metaclass_info = sym.node

if not isinstance(metaclass_info, TypeInfo) or metaclass_info.tuple_type is not None:
self.fail(f'Invalid metaclass "{metaclass_name}"', metaclass_expr)
self.fail(
f'Invalid metaclass "{metaclass_name}"', metaclass_expr, code=codes.METACLASS
)
return None, False, False
if not metaclass_info.is_metaclass():
self.fail(
'Metaclasses not inheriting from "type" are not supported', metaclass_expr
'Metaclasses not inheriting from "type" are not supported',
metaclass_expr,
code=codes.METACLASS,
)
return None, False, False
inst = fill_typevars(metaclass_info)
Expand Down
20 changes: 10 additions & 10 deletions test-data/unit/check-classes.test
Original file line number Diff line number Diff line change
Expand Up @@ -4558,7 +4558,7 @@ class C(B):
class X(type): pass
class Y(type): pass
class A(metaclass=X): pass
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
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 - own metaclass __main__.Y is not a subclass of __main__.X

[case testMetaclassNoTypeReveal]
class M:
Expand Down Expand Up @@ -5552,8 +5552,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: 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
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 - own metaclass __main__.M is not a subclass of __main__.M1
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 - own metaclass __main__.M is not a subclass of __main__.M1
[builtins fixtures/tuple.pyi]

[case testSixMetaclassAny]
Expand Down Expand Up @@ -5671,7 +5671,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: Metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases
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 - own metaclass __main__.M is not a subclass of __main__.M1
[builtins fixtures/tuple.pyi]

[case testFutureMetaclassAny]
Expand Down Expand Up @@ -7100,17 +7100,17 @@ 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 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 - found metaclasses of bases: __main__.MyMeta1, __main__.MyMeta2
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 - found metaclasses of bases: __main__.MyMeta1, __main__.MyMeta2
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 - found metaclasses of bases: __main__.MyMeta2, __main__.MyMeta1

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 ChildOfConflict1(Conflict3): ... # E: Metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases - found metaclasses of bases: __main__.MyMeta2, __main__.MyMeta1
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 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 - own metaclass __main__.ConflictingMeta is not a subclass of __main__.MyMeta2

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
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 - own metaclass __main__.ConflictingMeta is not a subclass of __main__.CorrectMeta, __main__.MyMeta2
...

[case testGenericOverride]
Expand Down
32 changes: 32 additions & 0 deletions test-data/unit/check-errorcodes.test
Original file line number Diff line number Diff line change
Expand Up @@ -1196,3 +1196,35 @@ def f(x: str) -> TypeIs[int]: # E: Narrowed type "int" is not a subtype of inpu
pass

[builtins fixtures/tuple.pyi]

[case testDynamicMetaclass]
class A(metaclass=type(tuple)): pass # E: Dynamic metaclass not supported for "A" [metaclass]
[builtins fixtures/tuple.pyi]

[case testMetaclassOfTypeAny]
# mypy: disallow-subclassing-any=True
from typing import Any
foo: Any = ...
class A(metaclass=foo): pass # E: Class cannot use "foo" as a metaclass (has type "Any") [metaclass]

[case testMetaclassOfWrongType]
class Foo:
bar = 1
class A2(metaclass=Foo.bar): pass # E: Invalid metaclass "Foo.bar" [metaclass]

[case testMetaclassNotTypeSubclass]
class M: pass
class A(metaclass=M): pass # E: Metaclasses not inheriting from "type" are not supported [metaclass]

[case testMultipleMetaclasses]
import six
class M1(type): pass

@six.add_metaclass(M1)
class A1(metaclass=M1): pass # E: Multiple metaclass definitions [metaclass]

class A2(six.with_metaclass(M1), metaclass=M1): pass # E: Multiple metaclass definitions [metaclass]

@six.add_metaclass(M1)
class A3(six.with_metaclass(M1)): pass # E: Multiple metaclass definitions [metaclass]
[builtins fixtures/tuple.pyi]
12 changes: 8 additions & 4 deletions test-data/unit/fine-grained.test
Original file line number Diff line number Diff line change
Expand Up @@ -2936,10 +2936,12 @@ a.py:6: error: Argument 1 to "f" has incompatible type "Type[B]"; expected "M"

[case testFineMetaclassRecalculation]
import a

[file a.py]
from b import B
class M2(type): pass
class D(B, metaclass=M2): pass

[file b.py]
import c
class B: pass
Expand All @@ -2949,27 +2951,29 @@ import c
class B(metaclass=c.M): pass

[file c.py]
class M(type):
pass
class M(type): pass
[out]
==
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
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 - own metaclass a.M2 is not a subclass of c.M

[case testFineMetaclassDeclaredUpdate]
import a

[file a.py]
import b
class B(metaclass=b.M): pass
class D(B, metaclass=b.M2): pass

[file b.py]
class M(type): pass
class M2(M): pass

[file b.py.2]
class M(type): pass
class M2(type): pass
[out]
==
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
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 - own metaclass b.M2 is not a subclass of b.M

[case testFineMetaclassRemoveFromClass]
import a
Expand Down
Loading