Skip to content

Commit 0b612cd

Browse files
authored
Fix infinite semantic analysis loop when using from_queryset (#2935)
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
1 parent 5f88db2 commit 0b612cd

File tree

2 files changed

+49
-3
lines changed

2 files changed

+49
-3
lines changed

mypy_django_plugin/transformers/managers.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -334,9 +334,12 @@ def create_new_manager_class_from_from_queryset_method(ctx: DynamicClassDefConte
334334
new_manager_info = create_manager_info_from_from_queryset_call(semanal_api, ctx.call, ctx.name)
335335
if new_manager_info is None:
336336
if not ctx.api.final_iteration:
337-
# XXX: hack for python/mypy#17402
338-
ph = PlaceholderNode(ctx.api.qualified_name(ctx.name), ctx.call, ctx.call.line, becomes_typeinfo=True)
339-
ctx.api.add_symbol_table_node(ctx.name, SymbolTableNode(GDEF, ph))
337+
# Only add PlaceholderNode if it doesn't already exist to prevent
338+
# infinite semantic analysis iterations (fixes #2373)
339+
if not (manager_sym and isinstance(manager_sym.node, PlaceholderNode)):
340+
# XXX: hack for python/mypy#17402
341+
ph = PlaceholderNode(ctx.api.qualified_name(ctx.name), ctx.call, ctx.call.line, becomes_typeinfo=True)
342+
ctx.api.add_symbol_table_node(ctx.name, SymbolTableNode(GDEF, ph))
340343
ctx.api.defer()
341344
return
342345

tests/typecheck/managers/querysets/test_from_queryset.yml

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -999,3 +999,46 @@
999999
field = models.CharField()
10001000
b = models.ForeignKey(B, on_delete=models.CASCADE)
10011001
objects = Manager()
1002+
1003+
# Regression test for #2373: Ensure from_queryset handles repeated deferrals
1004+
# correctly. When create_manager_info_from_from_queryset_call returns None due
1005+
# to forward references, PlaceholderNode should only be added once, not on
1006+
# every iteration.
1007+
- case: test_from_queryset_repeated_deferral_with_forward_metaclass
1008+
main: |
1009+
from typing_extensions import reveal_type
1010+
from myapp.models import MyModel
1011+
reveal_type(MyModel.objects.custom_method())
1012+
out: |
1013+
main:3: error: Access to generic instance variables via class is ambiguous [misc]
1014+
main:3: note: Revealed type is "builtins.str"
1015+
mypy_config: |
1016+
[mypy.plugins.django-stubs]
1017+
django_settings_module = myapp
1018+
files:
1019+
- path: myapp/__init__.py
1020+
- path: myapp/models.py
1021+
content: |
1022+
from typing import TypeVar
1023+
from django.db import models
1024+
from django.db.models.manager import Manager
1025+
1026+
M = TypeVar("M", bound=models.Model, covariant=True)
1027+
1028+
# Forward reference to metaclass triggers deferral, causing
1029+
# create_manager_info_from_from_queryset_call to return None
1030+
# on initial passes. The fix ensures PlaceholderNode is not
1031+
# re-added on each iteration, preventing progress flag from
1032+
# being set repeatedly.
1033+
class CustomQuerySet(models.QuerySet[M], metaclass=ForwardMCS):
1034+
def custom_method(self) -> str:
1035+
return "test"
1036+
1037+
CustomManager = Manager.from_queryset(CustomQuerySet)
1038+
1039+
class MyModel(models.Model):
1040+
objects = CustomManager()
1041+
1042+
# Forward-referenced metaclass defined after use
1043+
class ForwardMCS(type):
1044+
pass

0 commit comments

Comments
 (0)