Skip to content

Commit be3a646

Browse files
committed
Introduce new error code metaclass
1 parent 56b7af6 commit be3a646

File tree

5 files changed

+69
-4
lines changed

5 files changed

+69
-4
lines changed

docs/source/error_code_list.rst

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,32 @@ You can use :py:data:`~typing.Callable` as the type for callable objects:
221221
for x in objs:
222222
f(x)
223223
224+
.. _code-metaclass:
225+
226+
Check the validity of a class's metaclass [metaclass]
227+
-----------------------------------------------------
228+
229+
Mypy checks whether the metaclass of a class is valid. The metaclass
230+
must be a subclass of ``type``. Further, the class hierarchy must yield
231+
a consistent metaclass. For more details, see the
232+
`Python documentation <https://docs.python.org/3.13/reference/datamodel.html#determining-the-appropriate-metaclass>`_
233+
234+
Example with an error:
235+
236+
.. code-block:: python
237+
238+
class GoodMeta(type):
239+
pass
240+
241+
class BadMeta:
242+
pass
243+
244+
class A1(metaclass=GoodMeta): # OK
245+
pass
246+
247+
class A2(metaclass=BadMeta): # Error: Metaclasses not inheriting from "type" are not supported [metaclass]
248+
pass
249+
224250
.. _code-var-annotated:
225251

226252
Require annotation if variable type is unclear [var-annotated]

mypy/checker.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2851,6 +2851,7 @@ def check_metaclass_compatibility(self, typ: TypeInfo) -> None:
28512851
"(non-strict) subclass of the metaclasses of all its bases - "
28522852
f"{conflict_info}",
28532853
typ,
2854+
code=codes.METACLASS,
28542855
)
28552856

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

mypy/errorcodes.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,11 @@ def __hash__(self) -> int:
261261
"General",
262262
default_enabled=False,
263263
)
264+
METACLASS: Final[ErrorCode] = ErrorCode(
265+
"metaclass",
266+
"Ensure that metaclass is valid",
267+
"General",
268+
)
264269

265270
# Syntax errors are often blocking.
266271
SYNTAX: Final[ErrorCode] = ErrorCode("syntax", "Report syntax errors", "General")

mypy/semanal.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2592,7 +2592,7 @@ def infer_metaclass_and_bases_from_compat_helpers(self, defn: ClassDef) -> None:
25922592
if len(metas) == 0:
25932593
return
25942594
if len(metas) > 1:
2595-
self.fail("Multiple metaclass definitions", defn)
2595+
self.fail("Multiple metaclass definitions", defn, code=codes.METACLASS)
25962596
return
25972597
defn.metaclass = metas.pop()
25982598

@@ -2648,7 +2648,7 @@ def get_declared_metaclass(
26482648
elif isinstance(metaclass_expr, MemberExpr):
26492649
metaclass_name = get_member_expr_fullname(metaclass_expr)
26502650
if metaclass_name is None:
2651-
self.fail(f'Dynamic metaclass not supported for "{name}"', metaclass_expr)
2651+
self.fail(f'Dynamic metaclass not supported for "{name}"', metaclass_expr, code=codes.METACLASS)
26522652
return None, False, True
26532653
sym = self.lookup_qualified(metaclass_name, metaclass_expr)
26542654
if sym is None:
@@ -2659,6 +2659,7 @@ def get_declared_metaclass(
26592659
self.fail(
26602660
f'Class cannot use "{sym.node.name}" as a metaclass (has type "Any")',
26612661
metaclass_expr,
2662+
code=codes.METACLASS,
26622663
)
26632664
return None, False, True
26642665
if isinstance(sym.node, PlaceholderNode):
@@ -2676,11 +2677,13 @@ def get_declared_metaclass(
26762677
metaclass_info = sym.node
26772678

26782679
if not isinstance(metaclass_info, TypeInfo) or metaclass_info.tuple_type is not None:
2679-
self.fail(f'Invalid metaclass "{metaclass_name}"', metaclass_expr)
2680+
self.fail(f'Invalid metaclass "{metaclass_name}"', metaclass_expr, code=codes.METACLASS)
26802681
return None, False, False
26812682
if not metaclass_info.is_metaclass():
26822683
self.fail(
2683-
'Metaclasses not inheriting from "type" are not supported', metaclass_expr
2684+
'Metaclasses not inheriting from "type" are not supported',
2685+
metaclass_expr,
2686+
code=codes.METACLASS,
26842687
)
26852688
return None, False, False
26862689
inst = fill_typevars(metaclass_info)

test-data/unit/check-errorcodes.test

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1195,4 +1195,34 @@ from typing_extensions import TypeIs
11951195
def f(x: str) -> TypeIs[int]: # E: Narrowed type "int" is not a subtype of input type "str" [narrowed-type-not-subtype]
11961196
pass
11971197

1198+
[case testDynamicMetaclass]
1199+
class A(metaclass=type(tuple)): pass # E: Dynamic metaclass not supported for "A" [metaclass]
1200+
1201+
[case testMetaclassOfTypeAny]
1202+
# mypy: disallow-subclassing-any=True
1203+
from typing import Any
1204+
foo: Any = ...
1205+
class A(metaclass=foo): pass # E: Class cannot use "foo" as a metaclass (has type "Any") [metaclass]
1206+
1207+
[case testMetaclassOfWrongType]
1208+
class Foo:
1209+
bar = 1
1210+
class A2(metaclass=Foo.bar): pass # E: Invalid metaclass "Foo.bar" [metaclass]
1211+
1212+
[case testMetaclassNotTypeSubclass]
1213+
class M: pass
1214+
class A(metaclass=M): pass # E: Metaclasses not inheriting from "type" are not supported [metaclass]
1215+
1216+
[case testMultipleMetaclasses]
1217+
import six
1218+
class M1(type): pass
1219+
1220+
@six.add_metaclass(M1)
1221+
class A1(metaclass=M1): pass # E: Multiple metaclass definitions [metaclass]
1222+
1223+
class A2(six.with_metaclass(M1), metaclass=M1): pass # E: Multiple metaclass definitions [metaclass]
1224+
1225+
@six.add_metaclass(M1)
1226+
class A3(six.with_metaclass(M1)): pass # E: Multiple metaclass definitions [metaclass]
1227+
11981228
[builtins fixtures/tuple.pyi]

0 commit comments

Comments
 (0)