From e0084311fe497a17805db2fbdfeeb0c05601b170 Mon Sep 17 00:00:00 2001 From: Rifa Ferzana <78103780+Rifa-111@users.noreply.github.com> Date: Sun, 8 Nov 2026 19:01:49 +0000 Subject: [PATCH 1/4] Fix placeholder qualified names during semantic analysis (#22326) --- mypy/semanal.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/mypy/semanal.py b/mypy/semanal.py index c079a865af1d..cfd249059d2b 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -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 From b5dd3d7c1b995333376bb7c7da760ec9a54aa24c Mon Sep 17 00:00:00 2001 From: Rifa Ferzana <78103780+Rifa-111@users.noreply.github.com> Date: Sun, 8 Nov 2026 19:03:09 +0000 Subject: [PATCH 2/4] Support deferred dynamic class hook test fixture --- .../unit/plugins/dyn_class_from_method.py | 33 ++++++++++++------- 1 file changed, 22 insertions(+), 11 deletions(-) diff --git a/test-data/unit/plugins/dyn_class_from_method.py b/test-data/unit/plugins/dyn_class_from_method.py index 2630b16be66e..572a480ceb6f 100644 --- a/test-data/unit/plugins/dyn_class_from_method.py +++ b/test-data/unit/plugins/dyn_class_from_method.py @@ -31,23 +31,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) From d020e3143e5011982b951282119974d350c66b21 Mon Sep 17 00:00:00 2001 From: Rifa Ferzana <78103780+Rifa-111@users.noreply.github.com> Date: Sun, 8 Nov 2026 19:03:11 +0000 Subject: [PATCH 3/4] Add regression coverage for imported placeholder dynamic class hooks --- test-data/unit/check-custom-plugin.test | 52 +++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/test-data/unit/check-custom-plugin.test b/test-data/unit/check-custom-plugin.test index 504c15314553..52ee094e9b7c 100644 --- a/test-data/unit/check-custom-plugin.test +++ b/test-data/unit/check-custom-plugin.test @@ -723,6 +723,58 @@ class GenericQuerySet(Generic[T]): \[mypy] plugins=/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=/test-data/unit/plugins/dyn_class_from_method.py + [case testBaseClassPluginHookWorksIncremental] # flags: --config-file tmp/mypy.ini import a From 1a8ca7e2930c05f9a0920d8525eb79abc8ac2e56 Mon Sep 17 00:00:00 2001 From: Rifa Ferzana <78103780+Rifa-111@users.noreply.github.com> Date: Sun, 8 Nov 2026 19:22:50 +0000 Subject: [PATCH 4/4] Import PlaceholderNode for deferred dynamic class regression fixture --- test-data/unit/plugins/dyn_class_from_method.py | 1 + 1 file changed, 1 insertion(+) diff --git a/test-data/unit/plugins/dyn_class_from_method.py b/test-data/unit/plugins/dyn_class_from_method.py index 572a480ceb6f..e3721b51c1ae 100644 --- a/test-data/unit/plugins/dyn_class_from_method.py +++ b/test-data/unit/plugins/dyn_class_from_method.py @@ -9,6 +9,7 @@ IndexExpr, MemberExpr, NameExpr, + PlaceholderNode, RefExpr, SymbolTable, SymbolTableNode,