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

Fix crash on Any metaclass in incremental mode #14495

Merged
merged 1 commit into from
Jan 22, 2023
Merged
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
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