Skip to content

Commit

Permalink
Introduce new error code metaclass
Browse files Browse the repository at this point in the history
  • Loading branch information
robsdedude committed Nov 14, 2022
1 parent 56b7af6 commit be3a646
Show file tree
Hide file tree
Showing 5 changed files with 69 additions and 4 deletions.
26 changes: 26 additions & 0 deletions docs/source/error_code_list.rst
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,32 @@ 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>`_

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
1 change: 1 addition & 0 deletions mypy/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -2851,6 +2851,7 @@ def check_metaclass_compatibility(self, typ: TypeInfo) -> None:
"(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
5 changes: 5 additions & 0 deletions mypy/errorcodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,11 @@ 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
11 changes: 7 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,7 @@ 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 +2659,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 +2677,13 @@ 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
30 changes: 30 additions & 0 deletions test-data/unit/check-errorcodes.test
Original file line number Diff line number Diff line change
Expand Up @@ -1195,4 +1195,34 @@ from typing_extensions import TypeIs
def f(x: str) -> TypeIs[int]: # E: Narrowed type "int" is not a subtype of input type "str" [narrowed-type-not-subtype]
pass

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

[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]

0 comments on commit be3a646

Please sign in to comment.