Skip to content

Commit dfbaff7

Browse files
authored
Defer all types whos metaclass is not ready (#13579)
1 parent fd2d684 commit dfbaff7

File tree

3 files changed

+103
-29
lines changed

3 files changed

+103
-29
lines changed

mypy/semanal.py

Lines changed: 47 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -606,14 +606,18 @@ def add_implicit_module_attrs(self, file_node: MypyFile) -> None:
606606
if not sym:
607607
continue
608608
node = sym.node
609-
assert isinstance(node, TypeInfo)
609+
if not isinstance(node, TypeInfo):
610+
self.defer(node)
611+
return
610612
typ = Instance(node, [self.str_type()])
611613
elif name == "__annotations__":
612614
sym = self.lookup_qualified("__builtins__.dict", Context(), suppress_errors=True)
613615
if not sym:
614616
continue
615617
node = sym.node
616-
assert isinstance(node, TypeInfo)
618+
if not isinstance(node, TypeInfo):
619+
self.defer(node)
620+
return
617621
typ = Instance(node, [self.str_type(), AnyType(TypeOfAny.special_form)])
618622
else:
619623
assert t is not None, f"type should be specified for {name}"
@@ -1374,7 +1378,7 @@ def analyze_class(self, defn: ClassDef) -> None:
13741378
defn.base_type_exprs.extend(defn.removed_base_type_exprs)
13751379
defn.removed_base_type_exprs.clear()
13761380

1377-
self.update_metaclass(defn)
1381+
self.infer_metaclass_and_bases_from_compat_helpers(defn)
13781382

13791383
bases = defn.base_type_exprs
13801384
bases, tvar_defs, is_protocol = self.clean_up_bases_and_infer_type_variables(
@@ -1390,20 +1394,25 @@ def analyze_class(self, defn: ClassDef) -> None:
13901394
self.defer()
13911395

13921396
self.analyze_class_keywords(defn)
1393-
result = self.analyze_base_classes(bases)
1394-
1395-
if result is None or self.found_incomplete_ref(tag):
1397+
bases_result = self.analyze_base_classes(bases)
1398+
if bases_result is None or self.found_incomplete_ref(tag):
13961399
# Something was incomplete. Defer current target.
13971400
self.mark_incomplete(defn.name, defn)
13981401
return
13991402

1400-
base_types, base_error = result
1403+
base_types, base_error = bases_result
14011404
if any(isinstance(base, PlaceholderType) for base, _ in base_types):
14021405
# We need to know the TypeInfo of each base to construct the MRO. Placeholder types
14031406
# are okay in nested positions, since they can't affect the MRO.
14041407
self.mark_incomplete(defn.name, defn)
14051408
return
14061409

1410+
declared_metaclass, should_defer = self.get_declared_metaclass(defn.name, defn.metaclass)
1411+
if should_defer or self.found_incomplete_ref(tag):
1412+
# Metaclass was not ready. Defer current target.
1413+
self.mark_incomplete(defn.name, defn)
1414+
return
1415+
14071416
if self.analyze_typeddict_classdef(defn):
14081417
if defn.info:
14091418
self.setup_type_vars(defn, tvar_defs)
@@ -1422,7 +1431,7 @@ def analyze_class(self, defn: ClassDef) -> None:
14221431
with self.scope.class_scope(defn.info):
14231432
self.configure_base_classes(defn, base_types)
14241433
defn.info.is_protocol = is_protocol
1425-
self.analyze_metaclass(defn)
1434+
self.recalculate_metaclass(defn, declared_metaclass)
14261435
defn.info.runtime_protocol = False
14271436
for decorator in defn.decorators:
14281437
self.analyze_class_decorator(defn, decorator)
@@ -1968,7 +1977,7 @@ def calculate_class_mro(
19681977
if hook:
19691978
hook(ClassDefContext(defn, FakeExpression(), self))
19701979

1971-
def update_metaclass(self, defn: ClassDef) -> None:
1980+
def infer_metaclass_and_bases_from_compat_helpers(self, defn: ClassDef) -> None:
19721981
"""Lookup for special metaclass declarations, and update defn fields accordingly.
19731982
19741983
* six.with_metaclass(M, B1, B2, ...)
@@ -2046,30 +2055,33 @@ def is_base_class(self, t: TypeInfo, s: TypeInfo) -> bool:
20462055
visited.add(base.type)
20472056
return False
20482057

2049-
def analyze_metaclass(self, defn: ClassDef) -> None:
2050-
if defn.metaclass:
2058+
def get_declared_metaclass(
2059+
self, name: str, metaclass_expr: Expression | None
2060+
) -> tuple[Instance | None, bool]:
2061+
"""Returns either metaclass instance or boolean whether we should defer."""
2062+
declared_metaclass = None
2063+
if metaclass_expr:
20512064
metaclass_name = None
2052-
if isinstance(defn.metaclass, NameExpr):
2053-
metaclass_name = defn.metaclass.name
2054-
elif isinstance(defn.metaclass, MemberExpr):
2055-
metaclass_name = get_member_expr_fullname(defn.metaclass)
2065+
if isinstance(metaclass_expr, NameExpr):
2066+
metaclass_name = metaclass_expr.name
2067+
elif isinstance(metaclass_expr, MemberExpr):
2068+
metaclass_name = get_member_expr_fullname(metaclass_expr)
20562069
if metaclass_name is None:
2057-
self.fail(f'Dynamic metaclass not supported for "{defn.name}"', defn.metaclass)
2058-
return
2059-
sym = self.lookup_qualified(metaclass_name, defn.metaclass)
2070+
self.fail(f'Dynamic metaclass not supported for "{name}"', metaclass_expr)
2071+
return None, False
2072+
sym = self.lookup_qualified(metaclass_name, metaclass_expr)
20602073
if sym is None:
20612074
# Probably a name error - it is already handled elsewhere
2062-
return
2075+
return None, False
20632076
if isinstance(sym.node, Var) and isinstance(get_proper_type(sym.node.type), AnyType):
20642077
# 'Any' metaclass -- just ignore it.
20652078
#
20662079
# TODO: A better approach would be to record this information
20672080
# and assume that the type object supports arbitrary
20682081
# attributes, similar to an 'Any' base class.
2069-
return
2082+
return None, False
20702083
if isinstance(sym.node, PlaceholderNode):
2071-
self.defer(defn)
2072-
return
2084+
return None, True # defer later in the caller
20732085

20742086
# Support type aliases, like `_Meta: TypeAlias = type`
20752087
if (
@@ -2083,16 +2095,20 @@ def analyze_metaclass(self, defn: ClassDef) -> None:
20832095
metaclass_info = sym.node
20842096

20852097
if not isinstance(metaclass_info, TypeInfo) or metaclass_info.tuple_type is not None:
2086-
self.fail(f'Invalid metaclass "{metaclass_name}"', defn.metaclass)
2087-
return
2098+
self.fail(f'Invalid metaclass "{metaclass_name}"', metaclass_expr)
2099+
return None, False
20882100
if not metaclass_info.is_metaclass():
20892101
self.fail(
2090-
'Metaclasses not inheriting from "type" are not supported', defn.metaclass
2102+
'Metaclasses not inheriting from "type" are not supported', metaclass_expr
20912103
)
2092-
return
2104+
return None, False
20932105
inst = fill_typevars(metaclass_info)
20942106
assert isinstance(inst, Instance)
2095-
defn.info.declared_metaclass = inst
2107+
declared_metaclass = inst
2108+
return declared_metaclass, False
2109+
2110+
def recalculate_metaclass(self, defn: ClassDef, declared_metaclass: Instance | None) -> None:
2111+
defn.info.declared_metaclass = declared_metaclass
20962112
defn.info.metaclass_type = defn.info.calculate_metaclass_type()
20972113
if any(info.is_protocol for info in defn.info.mro):
20982114
if (
@@ -2104,13 +2120,15 @@ def analyze_metaclass(self, defn: ClassDef) -> None:
21042120
abc_meta = self.named_type_or_none("abc.ABCMeta", [])
21052121
if abc_meta is not None: # May be None in tests with incomplete lib-stub.
21062122
defn.info.metaclass_type = abc_meta
2107-
if defn.info.metaclass_type is None:
2123+
if declared_metaclass is not None and defn.info.metaclass_type is None:
21082124
# Inconsistency may happen due to multiple baseclasses even in classes that
21092125
# do not declare explicit metaclass, but it's harder to catch at this stage
21102126
if defn.metaclass is not None:
21112127
self.fail(f'Inconsistent metaclass structure for "{defn.name}"', defn)
21122128
else:
2113-
if defn.info.metaclass_type.type.has_base("enum.EnumMeta"):
2129+
if defn.info.metaclass_type and defn.info.metaclass_type.type.has_base(
2130+
"enum.EnumMeta"
2131+
):
21142132
defn.info.is_enum = True
21152133
if defn.type_vars:
21162134
self.fail("Enum class cannot be generic", defn)

test-data/unit/check-classes.test

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6676,6 +6676,48 @@ class MyMetaClass(type):
66766676
class MyClass(metaclass=MyMetaClass):
66776677
pass
66786678

6679+
6680+
[case testMetaclassPlaceholderNode]
6681+
from sympy.assumptions import ManagedProperties
6682+
from sympy.ops import AssocOp
6683+
reveal_type(AssocOp.x) # N: Revealed type is "sympy.basic.Basic"
6684+
reveal_type(AssocOp.y) # N: Revealed type is "builtins.int"
6685+
6686+
[file sympy/__init__.py]
6687+
6688+
[file sympy/assumptions.py]
6689+
from .basic import Basic
6690+
class ManagedProperties(type):
6691+
x: Basic
6692+
y: int
6693+
# The problem is with the next line,
6694+
# it creates the following order (classname, metaclass):
6695+
# 1. Basic NameExpr(ManagedProperties)
6696+
# 2. AssocOp None
6697+
# 3. ManagedProperties None
6698+
# 4. Basic NameExpr(ManagedProperties [sympy.assumptions.ManagedProperties])
6699+
# So, `AssocOp` will still have `metaclass_type` as `None`
6700+
# and all its `mro` types will have `declared_metaclass` as `None`.
6701+
from sympy.ops import AssocOp
6702+
6703+
[file sympy/basic.py]
6704+
from .assumptions import ManagedProperties
6705+
class Basic(metaclass=ManagedProperties): ...
6706+
6707+
[file sympy/ops.py]
6708+
from sympy.basic import Basic
6709+
class AssocOp(Basic): ...
6710+
6711+
[case testMetaclassSubclassSelf]
6712+
# This does not make much sense, but we must not crash:
6713+
import a
6714+
[file m.py]
6715+
from a import A # E: Module "a" has no attribute "A"
6716+
class Meta(A): pass
6717+
[file a.py]
6718+
from m import Meta
6719+
class A(metaclass=Meta): pass
6720+
66796721
[case testGenericOverride]
66806722
from typing import Generic, TypeVar, Any
66816723

test-data/unit/check-modules.test

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2904,6 +2904,20 @@ from . import m as m
29042904
[file p/m.py]
29052905
[builtins fixtures/list.pyi]
29062906

2907+
[case testSpecialModulesNameImplicitAttr]
2908+
import typing
2909+
import builtins
2910+
import abc
2911+
2912+
reveal_type(abc.__name__) # N: Revealed type is "builtins.str"
2913+
reveal_type(builtins.__name__) # N: Revealed type is "builtins.str"
2914+
reveal_type(typing.__name__) # N: Revealed type is "builtins.str"
2915+
2916+
[case testSpecialAttrsAreAvaliableInClasses]
2917+
class Some:
2918+
name = __name__
2919+
reveal_type(Some.name) # N: Revealed type is "builtins.str"
2920+
29072921
[case testReExportAllInStub]
29082922
from m1 import C
29092923
from m1 import D # E: Module "m1" has no attribute "D"

0 commit comments

Comments
 (0)