From 5c5302d3cdebb23b285dc7e3e75b3bba01727509 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Sat, 21 Jan 2023 18:22:39 +0000 Subject: [PATCH] Fix crash on Any metaclass in incremental mode --- mypy/checkmember.py | 2 +- mypy/nodes.py | 7 +++++ mypy/semanal.py | 40 ++++++++++++++++----------- mypy/server/astdiff.py | 1 + mypy/subtypes.py | 2 +- test-data/unit/check-classes.test | 10 +++++-- test-data/unit/check-incremental.test | 11 ++++++++ test-data/unit/fine-grained.test | 2 -- 8 files changed, 52 insertions(+), 23 deletions(-) diff --git a/mypy/checkmember.py b/mypy/checkmember.py index 918ce7520454..f90a4f706a87 100644 --- a/mypy/checkmember.py +++ b/mypy/checkmember.py @@ -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 diff --git a/mypy/nodes.py b/mypy/nodes.py index 4a4de9d4503d..38639d553b3d 100644 --- a/mypy/nodes.py +++ b/mypy/nodes.py @@ -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", @@ -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) @@ -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", @@ -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 diff --git a/mypy/semanal.py b/mypy/semanal.py index f42eee28517e..5653aa4547c4 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -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) @@ -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) @@ -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 @@ -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 ( @@ -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 diff --git a/mypy/server/astdiff.py b/mypy/server/astdiff.py index 97f811384d37..012d395e632f 100644 --- a/mypy/server/astdiff.py +++ b/mypy/server/astdiff.py @@ -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. diff --git a/mypy/subtypes.py b/mypy/subtypes.py index 0214e7ae308a..4bf3672af740 100644 --- a/mypy/subtypes.py +++ b/mypy/subtypes.py @@ -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 diff --git a/test-data/unit/check-classes.test b/test-data/unit/check-classes.test index 9a38d8f344f7..f1af13923fd7 100644 --- a/test-data/unit/check-classes.test +++ b/test-data/unit/check-classes.test @@ -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): @@ -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" @@ -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 diff --git a/test-data/unit/check-incremental.test b/test-data/unit/check-incremental.test index 60917db041a1..1aff1ba2862f 100644 --- a/test-data/unit/check-incremental.test +++ b/test-data/unit/check-incremental.test @@ -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): ... diff --git a/test-data/unit/fine-grained.test b/test-data/unit/fine-grained.test index d4b2d3469871..ed33776af438 100644 --- a/test-data/unit/fine-grained.test +++ b/test-data/unit/fine-grained.test @@ -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 @@ -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]