From f8348e1954798792231ede13a0b17da1e8f0688d Mon Sep 17 00:00:00 2001 From: Sangjin Lee Date: Sun, 7 Dec 2025 19:16:49 +0900 Subject: [PATCH] Fix infinite semantic analysis loop when using from_queryset When `create_manager_info_from_from_queryset_call()` returns `None`, the code unconditionally called `add_symbol_table_node()` with a `PlaceholderNode` on every iteration. This sets mypy's `progress = True`, preventing the semantic analysis loop from terminating, resulting in "maximum semantic analysis iteration count reached" error. The fix checks if a `PlaceholderNode` already exists before adding a new one, preventing redundant symbol table modifications that trigger infinite iterations. Fixes #2373 --- mypy_django_plugin/transformers/managers.py | 9 ++-- .../managers/querysets/test_from_queryset.yml | 43 +++++++++++++++++++ 2 files changed, 49 insertions(+), 3 deletions(-) diff --git a/mypy_django_plugin/transformers/managers.py b/mypy_django_plugin/transformers/managers.py index 3fc665797..066584046 100644 --- a/mypy_django_plugin/transformers/managers.py +++ b/mypy_django_plugin/transformers/managers.py @@ -334,9 +334,12 @@ def create_new_manager_class_from_from_queryset_method(ctx: DynamicClassDefConte new_manager_info = create_manager_info_from_from_queryset_call(semanal_api, ctx.call, ctx.name) if new_manager_info is None: if not ctx.api.final_iteration: - # XXX: hack for python/mypy#17402 - ph = PlaceholderNode(ctx.api.qualified_name(ctx.name), ctx.call, ctx.call.line, becomes_typeinfo=True) - ctx.api.add_symbol_table_node(ctx.name, SymbolTableNode(GDEF, ph)) + # Only add PlaceholderNode if it doesn't already exist to prevent + # infinite semantic analysis iterations (fixes #2373) + if not (manager_sym and isinstance(manager_sym.node, PlaceholderNode)): + # XXX: hack for python/mypy#17402 + ph = PlaceholderNode(ctx.api.qualified_name(ctx.name), ctx.call, ctx.call.line, becomes_typeinfo=True) + ctx.api.add_symbol_table_node(ctx.name, SymbolTableNode(GDEF, ph)) ctx.api.defer() return diff --git a/tests/typecheck/managers/querysets/test_from_queryset.yml b/tests/typecheck/managers/querysets/test_from_queryset.yml index c2b291ee7..64b09caa7 100644 --- a/tests/typecheck/managers/querysets/test_from_queryset.yml +++ b/tests/typecheck/managers/querysets/test_from_queryset.yml @@ -999,3 +999,46 @@ field = models.CharField() b = models.ForeignKey(B, on_delete=models.CASCADE) objects = Manager() + +# Regression test for #2373: Ensure from_queryset handles repeated deferrals +# correctly. When create_manager_info_from_from_queryset_call returns None due +# to forward references, PlaceholderNode should only be added once, not on +# every iteration. +- case: test_from_queryset_repeated_deferral_with_forward_metaclass + main: | + from typing_extensions import reveal_type + from myapp.models import MyModel + reveal_type(MyModel.objects.custom_method()) + out: | + main:3: error: Access to generic instance variables via class is ambiguous [misc] + main:3: note: Revealed type is "builtins.str" + mypy_config: | + [mypy.plugins.django-stubs] + django_settings_module = myapp + files: + - path: myapp/__init__.py + - path: myapp/models.py + content: | + from typing import TypeVar + from django.db import models + from django.db.models.manager import Manager + + M = TypeVar("M", bound=models.Model, covariant=True) + + # Forward reference to metaclass triggers deferral, causing + # create_manager_info_from_from_queryset_call to return None + # on initial passes. The fix ensures PlaceholderNode is not + # re-added on each iteration, preventing progress flag from + # being set repeatedly. + class CustomQuerySet(models.QuerySet[M], metaclass=ForwardMCS): + def custom_method(self) -> str: + return "test" + + CustomManager = Manager.from_queryset(CustomQuerySet) + + class MyModel(models.Model): + objects = CustomManager() + + # Forward-referenced metaclass defined after use + class ForwardMCS(type): + pass