Skip to content
Open
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: 2 additions & 0 deletions mypy/semanal.py
Original file line number Diff line number Diff line change
Expand Up @@ -5972,6 +5972,8 @@ def bind_name_expr(self, expr: NameExpr, sym: SymbolTableNode) -> None:
):
self.fail(f'"{expr.name}" is a type variable and only valid in type context', expr)
elif isinstance(sym.node, PlaceholderNode):
# Preserve the qualified name for plugin hooks during import cycles.
expr.fullname = sym.fullname or ""
self.process_placeholder(expr.name, "name", expr)
else:
expr.kind = sym.kind
Expand Down
52 changes: 52 additions & 0 deletions test-data/unit/check-custom-plugin.test
Original file line number Diff line number Diff line change
Expand Up @@ -723,6 +723,58 @@ class GenericQuerySet(Generic[T]):
\[mypy]
plugins=<ROOT>/test-data/unit/plugins/dyn_class_from_method.py

[case testDynamicClassHookImportedPlaceholderCycle]
# Regression for https://github.com/python/mypy/issues/22326
# Adapted from the reproduction published by @ayushin in that issue.
# flags: --config-file tmp/mypy.ini
from models import Row

reveal_type(Row.objects.base_value()) # N: Revealed type is "builtins.int"
reveal_type(Row.objects.child_value()) # N: Revealed type is "builtins.str"
Row.objects.missing() # E: "ChildManager" has no attribute "missing"

[file models.py]
from managers import ChildManager

class Row:
objects = ChildManager()

[file managers.py]
from base_managers import BaseManager, BaseQuerySet

class ChildQuerySet(BaseQuerySet):
def child_value(self) -> str:
return "child"

_ChildManager = BaseManager.from_queryset(ChildQuerySet)

class ChildManager(_ChildManager):
pass

[file base_managers.py]
from typing import Any, Generic, TypeVar
from models import Row

T = TypeVar("T")

class QuerySet(Generic[T]):
@classmethod
def from_queryset(cls, queryset_cls: Any) -> Any: ...

class BaseQuerySet(QuerySet[Row]):
def base_value(self) -> int:
return 1

_BaseManager = QuerySet.from_queryset(BaseQuerySet)

class BaseManager(_BaseManager):
pass

[builtins fixtures/classmethod.pyi]
[file mypy.ini]
\\[mypy]
plugins=<ROOT>/test-data/unit/plugins/dyn_class_from_method.py

[case testBaseClassPluginHookWorksIncremental]
# flags: --config-file tmp/mypy.ini
import a
Expand Down
34 changes: 23 additions & 11 deletions test-data/unit/plugins/dyn_class_from_method.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
IndexExpr,
MemberExpr,
NameExpr,
PlaceholderNode,
RefExpr,
SymbolTable,
SymbolTableNode,
Expand All @@ -31,23 +32,34 @@ def get_dynamic_class_hook(


def add_info_hook(ctx: DynamicClassDefContext) -> None:
# Defer until the imported queryset has been fully analysed.
fullname = ctx.api.qualified_name(ctx.name)
existing = ctx.api.lookup_fully_qualified_or_none(fullname)
if existing is not None and isinstance(existing.node, TypeInfo):
return
assert isinstance(ctx.call.args[0], RefExpr)
queryset_fullname = ctx.call.args[0].fullname
symbol = (
ctx.api.lookup_fully_qualified_or_none(queryset_fullname)
if queryset_fullname
else None
)
if symbol is None or not isinstance(symbol.node, TypeInfo) or not symbol.node.mro:
assert not ctx.api.final_iteration
if existing is None or not isinstance(existing.node, PlaceholderNode):
placeholder = PlaceholderNode(fullname, ctx.call, ctx.call.line, becomes_typeinfo=True)
ctx.api.add_symbol_table_node(ctx.name, SymbolTableNode(GDEF, placeholder))
ctx.api.defer()
return
queryset_info = symbol.node
class_def = ClassDef(ctx.name, Block([]))
class_def.fullname = ctx.api.qualified_name(ctx.name)

class_def.fullname = fullname
info = TypeInfo(SymbolTable(), class_def, ctx.api.cur_mod_id)
class_def.info = info
assert isinstance(ctx.call.args[0], RefExpr)
queryset_type_fullname = ctx.call.args[0].fullname
queryset_node = ctx.api.lookup_fully_qualified_or_none(queryset_type_fullname)
assert queryset_node is not None
queryset_info = queryset_node.node
assert isinstance(queryset_info, TypeInfo)
obj = ctx.api.named_type("builtins.object")
info.mro = [info, queryset_info, obj.type]
info.mro = [info, *queryset_info.mro]
info.bases = [Instance(queryset_info, [])]
ctx.api.add_symbol_table_node(ctx.name, SymbolTableNode(GDEF, info))


def as_manager_hook(ctx: DynamicClassDefContext) -> None:
class_def = ClassDef(ctx.name, Block([]))
class_def.fullname = ctx.api.qualified_name(ctx.name)
Expand Down
Loading