Skip to content

Defer all types whos metaclass is not ready #13579

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

Merged
merged 13 commits into from
Sep 3, 2022
Merged
Show file tree
Hide file tree
Changes from 12 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
74 changes: 46 additions & 28 deletions mypy/semanal.py
Original file line number Diff line number Diff line change
Expand Up @@ -581,7 +581,11 @@ def refresh_partial(
def refresh_top_level(self, file_node: MypyFile) -> None:
"""Reanalyze a stale module top-level in fine-grained incremental mode."""
self.recurse_into_functions = False
self.add_implicit_module_attrs(file_node)
# We do it in the very last order, because of
# `builtins.dict <-> typing.Mapping <-> abc.ABCMeta`
# cyclic imports.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The problem is not the cyclic import itself, but that the method assumes that e.g. dict (for __annotations__) would be never marked incomplete (i.e. it may be not defined yet, but as soon as it is, it will be TypeInfo, rather than PlaceholderNode). This makes me think maybe a cleaner solution (if it works), would be just to replace assert with an if (and simply continue if it is a placeholder, like we do if the symbol is None). Can you try this?

Btw curiously this seem to only affect tests for now, it looks in real typeshed currently dict is not an instance of ABCMeta.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you try this?

Done, looks like it works! Let's wait to see what tests will show us :)

if file_node.fullname not in ("typing", "abc", "builtins"):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would instead re-use core_modules (defined in semanal_main.py) here and in other place. Also you may need to use a flag for this method to avoid an import cycle with the latter (like skip_implicit_attrs).

self.add_implicit_module_attrs(file_node)
for d in file_node.defs:
self.accept(d)
if file_node.fullname == "typing":
Expand Down Expand Up @@ -1374,7 +1378,7 @@ def analyze_class(self, defn: ClassDef) -> None:
defn.base_type_exprs.extend(defn.removed_base_type_exprs)
defn.removed_base_type_exprs.clear()

self.update_metaclass(defn)
self.infer_metaclass_and_bases_from_compat_helpers(defn)

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

self.analyze_class_keywords(defn)
result = self.analyze_base_classes(bases)

if result is None or self.found_incomplete_ref(tag):
bases_result = self.analyze_base_classes(bases)
if bases_result is None or self.found_incomplete_ref(tag):
# Something was incomplete. Defer current target.
self.mark_incomplete(defn.name, defn)
return

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

declared_metaclass, should_defer = 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)
return

if self.analyze_typeddict_classdef(defn):
if defn.info:
self.setup_type_vars(defn, tvar_defs)
Expand All @@ -1422,7 +1431,7 @@ def analyze_class(self, defn: ClassDef) -> None:
with self.scope.class_scope(defn.info):
self.configure_base_classes(defn, base_types)
defn.info.is_protocol = is_protocol
self.analyze_metaclass(defn)
self.recalculate_metaclass(defn, declared_metaclass)
defn.info.runtime_protocol = False
for decorator in defn.decorators:
self.analyze_class_decorator(defn, decorator)
Expand Down Expand Up @@ -1968,7 +1977,7 @@ def calculate_class_mro(
if hook:
hook(ClassDefContext(defn, FakeExpression(), self))

def update_metaclass(self, defn: ClassDef) -> None:
def infer_metaclass_and_bases_from_compat_helpers(self, defn: ClassDef) -> None:
"""Lookup for special metaclass declarations, and update defn fields accordingly.

* six.with_metaclass(M, B1, B2, ...)
Expand Down Expand Up @@ -2046,30 +2055,33 @@ def is_base_class(self, t: TypeInfo, s: TypeInfo) -> bool:
visited.add(base.type)
return False

def analyze_metaclass(self, defn: ClassDef) -> None:
if defn.metaclass:
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."""
declared_metaclass = None
if metaclass_expr:
metaclass_name = None
if isinstance(defn.metaclass, NameExpr):
metaclass_name = defn.metaclass.name
elif isinstance(defn.metaclass, MemberExpr):
metaclass_name = get_member_expr_fullname(defn.metaclass)
if isinstance(metaclass_expr, NameExpr):
metaclass_name = metaclass_expr.name
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 "{defn.name}"', defn.metaclass)
return
sym = self.lookup_qualified(metaclass_name, defn.metaclass)
self.fail(f'Dynamic metaclass not supported for "{name}"', metaclass_expr)
return None, False
sym = self.lookup_qualified(metaclass_name, metaclass_expr)
if sym is None:
# Probably a name error - it is already handled elsewhere
return
return None, False
if isinstance(sym.node, Var) and isinstance(get_proper_type(sym.node.type), AnyType):
# 'Any' metaclass -- just ignore it.
#
# TODO: A better approach would be to record this information
# and assume that the type object supports arbitrary
# attributes, similar to an 'Any' base class.
return
return None, False
if isinstance(sym.node, PlaceholderNode):
self.defer(defn)
return
return None, True # defer later in the caller

# Support type aliases, like `_Meta: TypeAlias = type`
if (
Expand All @@ -2083,16 +2095,20 @@ def analyze_metaclass(self, defn: ClassDef) -> None:
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}"', defn.metaclass)
return
self.fail(f'Invalid metaclass "{metaclass_name}"', metaclass_expr)
return None, False
if not metaclass_info.is_metaclass():
self.fail(
'Metaclasses not inheriting from "type" are not supported', defn.metaclass
'Metaclasses not inheriting from "type" are not supported', metaclass_expr
)
return
return None, False
inst = fill_typevars(metaclass_info)
assert isinstance(inst, Instance)
defn.info.declared_metaclass = inst
declared_metaclass = inst
return declared_metaclass, False

def recalculate_metaclass(self, defn: ClassDef, declared_metaclass: Instance | None) -> None:
defn.info.declared_metaclass = declared_metaclass
defn.info.metaclass_type = defn.info.calculate_metaclass_type()
if any(info.is_protocol for info in defn.info.mro):
if (
Expand All @@ -2104,13 +2120,15 @@ def analyze_metaclass(self, defn: ClassDef) -> None:
abc_meta = self.named_type_or_none("abc.ABCMeta", [])
if abc_meta is not None: # May be None in tests with incomplete lib-stub.
defn.info.metaclass_type = abc_meta
if defn.info.metaclass_type is None:
if declared_metaclass is not None and defn.info.metaclass_type is None:
# Inconsistency may happen due to multiple baseclasses even in classes that
# do not declare explicit metaclass, but it's harder to catch at this stage
if defn.metaclass is not None:
self.fail(f'Inconsistent metaclass structure for "{defn.name}"', defn)
else:
if defn.info.metaclass_type.type.has_base("enum.EnumMeta"):
if defn.info.metaclass_type and defn.info.metaclass_type.type.has_base(
"enum.EnumMeta"
):
defn.info.is_enum = True
if defn.type_vars:
self.fail("Enum class cannot be generic", defn)
Expand Down
14 changes: 14 additions & 0 deletions mypy/semanal_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,10 @@ def semantic_analysis_for_scc(graph: Graph, scc: list[str], errors: Errors) -> N
check_type_arguments(graph, scc, errors)
calculate_class_properties(graph, scc, errors)
check_blockers(graph, scc)
# Add some magic attrs to special modules:
for special_module in ("builtins", "typing", "abc"):
if special_module in scc:
process_special_implicit_attrs(graph[special_module])
# Clean-up builtins, so that TypeVar etc. are not accessible without importing.
if "builtins" in scc:
cleanup_builtin_scc(graph["builtins"])
Expand Down Expand Up @@ -223,6 +227,16 @@ def process_top_levels(graph: Graph, scc: list[str], patches: Patches) -> None:
final_iteration = not any_progress


def process_special_implicit_attrs(state: State) -> None:
# Add things like `__name__` and `__annotations__` to main modules.
analyzer = state.manager.semantic_analyzer
assert state.tree is not None
with analyzer.file_context(state.tree, state.options):
# We do it in the very last order,
# because of `dict <-> Mapping <-> ABCMeta` cyclic imports.
analyzer.add_implicit_module_attrs(state.tree)


def process_functions(graph: Graph, scc: list[str], patches: Patches) -> None:
# Process functions.
for module in scc:
Expand Down
42 changes: 42 additions & 0 deletions test-data/unit/check-classes.test
Original file line number Diff line number Diff line change
Expand Up @@ -6663,6 +6663,48 @@ class MyMetaClass(type):
class MyClass(metaclass=MyMetaClass):
pass


[case testMetaclassPlaceholderNode]
from sympy.assumptions import ManagedProperties
from sympy.ops import AssocOp
reveal_type(AssocOp.x) # N: Revealed type is "sympy.basic.Basic"
reveal_type(AssocOp.y) # N: Revealed type is "builtins.int"

[file sympy/__init__.py]

[file sympy/assumptions.py]
from .basic import Basic
class ManagedProperties(type):
x: Basic
y: int
# The problem is with the next line,
# it creates the following order (classname, metaclass):
# 1. Basic NameExpr(ManagedProperties)
# 2. AssocOp None
# 3. ManagedProperties None
# 4. Basic NameExpr(ManagedProperties [sympy.assumptions.ManagedProperties])
# So, `AssocOp` will still have `metaclass_type` as `None`
# and all its `mro` types will have `declared_metaclass` as `None`.
from sympy.ops import AssocOp

[file sympy/basic.py]
from .assumptions import ManagedProperties
class Basic(metaclass=ManagedProperties): ...

[file sympy/ops.py]
from sympy.basic import Basic
class AssocOp(Basic): ...

[case testMetaclassSubclassSelf]
# This does not make much sense, but we must not crash:
import a
[file m.py]
from a import A # E: Module "a" has no attribute "A"
class Meta(A): pass
[file a.py]
from m import Meta
class A(metaclass=Meta): pass

[case testGenericOverride]
from typing import Generic, TypeVar, Any

Expand Down
14 changes: 14 additions & 0 deletions test-data/unit/check-modules.test
Original file line number Diff line number Diff line change
Expand Up @@ -2904,6 +2904,20 @@ from . import m as m
[file p/m.py]
[builtins fixtures/list.pyi]

[case testSpecialModulesNameImplicitAttr]
import typing
import builtins
import abc

reveal_type(abc.__name__) # N: Revealed type is "builtins.str"
reveal_type(builtins.__name__) # N: Revealed type is "builtins.str"
reveal_type(typing.__name__) # N: Revealed type is "builtins.str"

[case testSpecialAttrsAreAvaliableInClasses]
class Some:
name = __name__
reveal_type(Some.name) # N: Revealed type is "builtins.str"

[case testReExportAllInStub]
from m1 import C
from m1 import D # E: Module "m1" has no attribute "D"
Expand Down