Skip to content

Commit

Permalink
Fix crash on Any metaclass in incremental mode (#14495)
Browse files Browse the repository at this point in the history
Fixes #14254

This essentially re-implements #13605
in a simpler way that also works in incremental mode. Also I decided to
set `meta_fallback_to_any` in case of errors, to match how we do this
for base classes.
  • Loading branch information
ilevkivskyi committed Jan 22, 2023
1 parent cc1bcc9 commit e8c844b
Show file tree
Hide file tree
Showing 8 changed files with 52 additions and 23 deletions.
2 changes: 1 addition & 1 deletion mypy/checkmember.py
Original file line number Diff line number Diff line change
Expand Up @@ -901,7 +901,7 @@ def analyze_class_attribute_access(
# For modules use direct symbol table lookup.
if not itype.extra_attrs.mod_name:
return itype.extra_attrs.attrs[name]
if info.fallback_to_any:
if info.fallback_to_any or info.meta_fallback_to_any:
return apply_class_attr_hook(mx, hook, AnyType(TypeOfAny.special_form))
return None

Expand Down
7 changes: 7 additions & 0 deletions mypy/nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -2800,6 +2800,7 @@ class is generic then it will be a type constructor of higher kind.
"inferring",
"is_enum",
"fallback_to_any",
"meta_fallback_to_any",
"type_vars",
"has_param_spec_type",
"bases",
Expand Down Expand Up @@ -2894,6 +2895,10 @@ class is generic then it will be a type constructor of higher kind.
# (and __setattr__), but without the __getattr__ method.
fallback_to_any: bool

# Same as above but for cases where metaclass has type Any. This will suppress
# all attribute errors only for *class object* access.
meta_fallback_to_any: bool

# Information related to type annotations.

# Generic type variable names (full names)
Expand Down Expand Up @@ -2963,6 +2968,7 @@ class is generic then it will be a type constructor of higher kind.
"is_abstract",
"is_enum",
"fallback_to_any",
"meta_fallback_to_any",
"is_named_tuple",
"is_newtype",
"is_protocol",
Expand Down Expand Up @@ -3002,6 +3008,7 @@ def __init__(self, names: SymbolTable, defn: ClassDef, module_name: str) -> None
self.is_final = False
self.is_enum = False
self.fallback_to_any = False
self.meta_fallback_to_any = False
self._promote = []
self.alt_promote = None
self.tuple_type = None
Expand Down
40 changes: 24 additions & 16 deletions mypy/semanal.py
Original file line number Diff line number Diff line change
Expand Up @@ -1579,7 +1579,9 @@ def analyze_class(self, defn: ClassDef) -> None:
self.mark_incomplete(defn.name, defn)
return

declared_metaclass, should_defer = self.get_declared_metaclass(defn.name, defn.metaclass)
declared_metaclass, should_defer, any_meta = self.get_declared_metaclass(
defn.name, defn.metaclass
)
if should_defer or self.found_incomplete_ref(tag):
# Metaclass was not ready. Defer current target.
self.mark_incomplete(defn.name, defn)
Expand All @@ -1599,6 +1601,8 @@ def analyze_class(self, defn: ClassDef) -> None:
self.setup_type_vars(defn, tvar_defs)
if base_error:
defn.info.fallback_to_any = True
if any_meta:
defn.info.meta_fallback_to_any = True

with self.scope.class_scope(defn.info):
self.configure_base_classes(defn, base_types)
Expand Down Expand Up @@ -2247,8 +2251,17 @@ def is_base_class(self, t: TypeInfo, s: TypeInfo) -> bool:

def get_declared_metaclass(
self, name: str, metaclass_expr: Expression | None
) -> tuple[Instance | None, bool]:
"""Returns either metaclass instance or boolean whether we should defer."""
) -> tuple[Instance | None, bool, bool]:
"""Get declared metaclass from metaclass expression.
Returns a tuple of three values:
* A metaclass instance or None
* A boolean indicating whether we should defer
* A boolean indicating whether we should set metaclass Any fallback
(either for Any metaclass or invalid/dynamic metaclass).
The two boolean flags can only be True if instance is None.
"""
declared_metaclass = None
if metaclass_expr:
metaclass_name = None
Expand All @@ -2258,25 +2271,20 @@ def get_declared_metaclass(
metaclass_name = get_member_expr_fullname(metaclass_expr)
if metaclass_name is None:
self.fail(f'Dynamic metaclass not supported for "{name}"', metaclass_expr)
return None, False
return None, False, True
sym = self.lookup_qualified(metaclass_name, metaclass_expr)
if sym is None:
# Probably a name error - it is already handled elsewhere
return None, False
return None, False, True
if isinstance(sym.node, Var) and isinstance(get_proper_type(sym.node.type), AnyType):
# Create a fake TypeInfo that fallbacks to `Any`, basically allowing
# all the attributes. Same thing as we do for `Any` base class.
any_info = self.make_empty_type_info(ClassDef(sym.node.name, Block([])))
any_info.fallback_to_any = True
any_info._fullname = sym.node.fullname
if self.options.disallow_subclassing_any:
self.fail(
f'Class cannot use "{any_info.fullname}" as a metaclass (has type "Any")',
f'Class cannot use "{sym.node.name}" as a metaclass (has type "Any")',
metaclass_expr,
)
return Instance(any_info, []), False
return None, False, True
if isinstance(sym.node, PlaceholderNode):
return None, True # defer later in the caller
return None, True, False # defer later in the caller

# Support type aliases, like `_Meta: TypeAlias = type`
if (
Expand All @@ -2291,16 +2299,16 @@ def get_declared_metaclass(

if not isinstance(metaclass_info, TypeInfo) or metaclass_info.tuple_type is not None:
self.fail(f'Invalid metaclass "{metaclass_name}"', metaclass_expr)
return None, False
return None, False, False
if not metaclass_info.is_metaclass():
self.fail(
'Metaclasses not inheriting from "type" are not supported', metaclass_expr
)
return None, False
return None, False, False
inst = fill_typevars(metaclass_info)
assert isinstance(inst, Instance)
declared_metaclass = inst
return declared_metaclass, False
return declared_metaclass, False, False

def recalculate_metaclass(self, defn: ClassDef, declared_metaclass: Instance | None) -> None:
defn.info.declared_metaclass = declared_metaclass
Expand Down
1 change: 1 addition & 0 deletions mypy/server/astdiff.py
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,7 @@ def snapshot_definition(node: SymbolNode | None, common: tuple[object, ...]) ->
node.is_enum,
node.is_protocol,
node.fallback_to_any,
node.meta_fallback_to_any,
node.is_named_tuple,
node.is_newtype,
# We need this to e.g. trigger metaclass calculation in subclasses.
Expand Down
2 changes: 1 addition & 1 deletion mypy/subtypes.py
Original file line number Diff line number Diff line change
Expand Up @@ -1167,7 +1167,7 @@ def find_member(
if isinstance(getattr_type, CallableType):
return getattr_type.ret_type
return getattr_type
if itype.type.fallback_to_any:
if itype.type.fallback_to_any or class_obj and itype.type.meta_fallback_to_any:
return AnyType(TypeOfAny.special_form)
if isinstance(v, TypeInfo):
# PEP 544 doesn't specify anything about such use cases. So we just try
Expand Down
10 changes: 7 additions & 3 deletions test-data/unit/check-classes.test
Original file line number Diff line number Diff line change
Expand Up @@ -4439,7 +4439,7 @@ def f(TB: Type[B]):
reveal_type(TB.x) # N: Revealed type is "builtins.int"

[case testMetaclassAsAny]
from typing import Any, ClassVar
from typing import Any, ClassVar, Type

MyAny: Any
class WithMeta(metaclass=MyAny):
Expand All @@ -4451,13 +4451,15 @@ reveal_type(WithMeta.x) # N: Revealed type is "builtins.int"
reveal_type(WithMeta().x) # N: Revealed type is "builtins.int"
WithMeta().m # E: "WithMeta" has no attribute "m"
WithMeta().a # E: "WithMeta" has no attribute "a"
t: Type[WithMeta]
t.unknown # OK

[case testMetaclassAsAnyWithAFlag]
# flags: --disallow-subclassing-any
from typing import Any, ClassVar
from typing import Any, ClassVar, Type

MyAny: Any
class WithMeta(metaclass=MyAny): # E: Class cannot use "__main__.MyAny" as a metaclass (has type "Any")
class WithMeta(metaclass=MyAny): # E: Class cannot use "MyAny" as a metaclass (has type "Any")
x: ClassVar[int]

reveal_type(WithMeta.a) # N: Revealed type is "Any"
Expand All @@ -4466,6 +4468,8 @@ reveal_type(WithMeta.x) # N: Revealed type is "builtins.int"
reveal_type(WithMeta().x) # N: Revealed type is "builtins.int"
WithMeta().m # E: "WithMeta" has no attribute "m"
WithMeta().a # E: "WithMeta" has no attribute "a"
t: Type[WithMeta]
t.unknown # OK

[case testMetaclassIterable]
from typing import Iterable, Iterator
Expand Down
11 changes: 11 additions & 0 deletions test-data/unit/check-incremental.test
Original file line number Diff line number Diff line change
Expand Up @@ -6348,3 +6348,14 @@ class C(B):
self.x = self.foo()
[out]
[out2]

[case testNoCrashIncrementalMetaAny]
import a
[file a.py]
from m import Foo
[file a.py.2]
from m import Foo
# touch
[file m.py]
from missing_module import Meta # type: ignore[import]
class Foo(metaclass=Meta): ...
2 changes: 0 additions & 2 deletions test-data/unit/fine-grained.test
Original file line number Diff line number Diff line change
Expand Up @@ -3124,7 +3124,6 @@ whatever: int
[out]
==
b.py:2: error: Name "c.M" is not defined
a.py:3: error: "Type[B]" has no attribute "x"

[case testFixMissingMetaclass]
import a
Expand All @@ -3143,7 +3142,6 @@ class M(type):
x: int
[out]
b.py:2: error: Name "c.M" is not defined
a.py:3: error: "Type[B]" has no attribute "x"
==

[case testGoodMetaclassSpoiled]
Expand Down

0 comments on commit e8c844b

Please sign in to comment.