Skip to content

Commit b269fcc

Browse files
committed
Always create manager at module level
When the manager is added at the class level, which happened when it was created inline in the model body, it's not possible to retrieve the manager again based on fullname. That lead to problems with inheritance and the default manager.
1 parent aa86858 commit b269fcc

File tree

4 files changed

+57
-46
lines changed

4 files changed

+57
-46
lines changed

mypy_django_plugin/lib/helpers.py

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,23 @@ def is_annotated_model_fullname(model_cls_fullname: str) -> bool:
208208
return model_cls_fullname.startswith(WITH_ANNOTATIONS_FULLNAME + "[")
209209

210210

211+
def create_type_info(name: str, module: str, bases: list[Instance]) -> TypeInfo:
212+
213+
# make new class expression
214+
classdef = ClassDef(name, Block([]))
215+
classdef.fullname = module + "." + name
216+
217+
# make new TypeInfo
218+
new_typeinfo = TypeInfo(SymbolTable(), classdef, module)
219+
new_typeinfo.bases = bases
220+
calculate_mro(new_typeinfo)
221+
new_typeinfo.calculate_metaclass_type()
222+
223+
classdef.info = new_typeinfo
224+
225+
return new_typeinfo
226+
227+
211228
def add_new_class_for_module(
212229
module: MypyFile,
213230
name: str,
@@ -217,15 +234,7 @@ def add_new_class_for_module(
217234
) -> TypeInfo:
218235
new_class_unique_name = checker.gen_unique_name(name, module.names)
219236

220-
# make new class expression
221-
classdef = ClassDef(new_class_unique_name, Block([]))
222-
classdef.fullname = module.fullname + "." + new_class_unique_name
223-
224-
# make new TypeInfo
225-
new_typeinfo = TypeInfo(SymbolTable(), classdef, module.fullname)
226-
new_typeinfo.bases = bases
227-
calculate_mro(new_typeinfo)
228-
new_typeinfo.calculate_metaclass_type()
237+
new_typeinfo = create_type_info(new_class_unique_name, module.fullname, bases)
229238

230239
# add fields
231240
if fields:
@@ -237,7 +246,6 @@ def add_new_class_for_module(
237246
MDEF, var, plugin_generated=True, no_serialize=no_serialize
238247
)
239248

240-
classdef.info = new_typeinfo
241249
module.names[new_class_unique_name] = SymbolTableNode(
242250
GDEF, new_typeinfo, plugin_generated=True, no_serialize=no_serialize
243251
)

mypy_django_plugin/transformers/managers.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -199,12 +199,6 @@ def create_new_manager_class_from_from_queryset_method(ctx: DynamicClassDefConte
199199
# So that the plugin will reparameterize the manager when it is constructed inside of a Model definition
200200
helpers.add_new_manager_base(semanal_api, new_manager_info.fullname)
201201

202-
# Insert the new manager (dynamic) class
203-
assert semanal_api.add_symbol_table_node(
204-
ctx.name,
205-
SymbolTableNode(GDEF, new_manager_info, plugin_generated=True),
206-
)
207-
208202

209203
def create_manager_info_from_from_queryset_call(
210204
api: SemanticAnalyzerPluginInterface, call_expr: CallExpr, name: Optional[str] = None
@@ -251,6 +245,12 @@ def create_manager_info_from_from_queryset_call(
251245
base_manager_info.metadata.setdefault("from_queryset_managers", {})
252246
base_manager_info.metadata["from_queryset_managers"][manager_fullname] = new_manager_info.fullname
253247

248+
# Add the new manager to the current module
249+
module = api.modules[api.cur_mod_id]
250+
module.names[name or manager_name] = SymbolTableNode(
251+
GDEF, new_manager_info, plugin_generated=True, no_serialize=False
252+
)
253+
254254
return new_manager_info
255255

256256

@@ -261,7 +261,7 @@ def create_manager_class(
261261
base_manager_instance = fill_typevars(base_manager_info)
262262
assert isinstance(base_manager_instance, Instance)
263263

264-
manager_info = api.basic_new_typeinfo(name, basetype_or_fallback=base_manager_instance, line=line)
264+
manager_info = helpers.create_type_info(name, api.cur_mod_id, bases=[base_manager_instance])
265265
manager_info.line = line
266266
manager_info.type_vars = base_manager_info.type_vars
267267
manager_info.defn.type_vars = base_manager_info.defn.type_vars

mypy_django_plugin/transformers/models.py

Lines changed: 3 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,7 @@
55
from django.db.models.fields.related import ForeignKey
66
from django.db.models.fields.reverse_related import ManyToManyRel, ManyToOneRel, OneToOneRel
77
from mypy.checker import TypeChecker
8-
from mypy.nodes import (
9-
ARG_STAR2,
10-
MDEF,
11-
Argument,
12-
AssignmentStmt,
13-
CallExpr,
14-
Context,
15-
FuncDef,
16-
NameExpr,
17-
SymbolTableNode,
18-
TypeInfo,
19-
Var,
20-
)
8+
from mypy.nodes import ARG_STAR2, Argument, AssignmentStmt, CallExpr, Context, FuncDef, NameExpr, TypeInfo, Var
219
from mypy.plugin import AnalyzeTypeContext, AttributeContext, CheckerPluginInterface, ClassDefContext
2210
from mypy.plugins import common
2311
from mypy.semanal import SemanticAnalyzer
@@ -29,7 +17,6 @@
2917
from mypy_django_plugin.errorcodes import MANAGER_MISSING
3018
from mypy_django_plugin.lib import fullnames, helpers
3119
from mypy_django_plugin.lib.fullnames import ANNOTATIONS_FULLNAME, ANY_ATTR_ALLOWED_CLASS_FULLNAME, MODEL_CLASS_FULLNAME
32-
from mypy_django_plugin.lib.helpers import add_new_class_for_module
3320
from mypy_django_plugin.transformers import fields
3421
from mypy_django_plugin.transformers.fields import get_field_descriptor_types
3522
from mypy_django_plugin.transformers.managers import create_manager_info_from_from_queryset_call
@@ -357,18 +344,7 @@ class MyModel(models.Model):
357344
if not isinstance(expr, CallExpr) or not isinstance(expr.callee, CallExpr):
358345
return None
359346

360-
new_manager_info = create_manager_info_from_from_queryset_call(self.api, expr.callee)
361-
362-
if new_manager_info:
363-
assert self.api.add_symbol_table_node(
364-
# We'll use `new_manager_info.name` instead of `manager_class_name` here
365-
# to handle possible name collisions, as it's unique.
366-
new_manager_info.name,
367-
# Note that the generated manager type is always inserted at module level
368-
SymbolTableNode(MDEF, new_manager_info, plugin_generated=True),
369-
)
370-
371-
return new_manager_info
347+
return create_manager_info_from_from_queryset_call(self.api, expr.callee)
372348

373349

374350
class AddDefaultManagerAttribute(ModelClassInitializer):
@@ -646,7 +622,7 @@ def get_or_create_annotated_type(
646622
else:
647623
annotated_model_type = api.named_generic_type(ANY_ATTR_ALLOWED_CLASS_FULLNAME, [])
648624

649-
annotated_typeinfo = add_new_class_for_module(
625+
annotated_typeinfo = helpers.add_new_class_for_module(
650626
model_module_file,
651627
type_name,
652628
bases=[model_type] if fields_dict is not None else [model_type, annotated_model_type],

tests/typecheck/managers/querysets/test_from_queryset.yml

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -362,12 +362,14 @@
362362
- case: test_queryset_in_model_class_body
363363
main: |
364364
from myapp.models import MyModel
365-
reveal_type(MyModel.objects) # N: Revealed type is "myapp.models.MyModel.MyManagerFromMyQuerySet[myapp.models.MyModel]"
365+
reveal_type(MyModel.objects) # N: Revealed type is "myapp.models.MyManagerFromMyQuerySet[myapp.models.MyModel]"
366+
reveal_type(MyModel._default_manager) # N: Revealed type is "myapp.models.MyManagerFromMyQuerySet[myapp.models.MyModel]"
366367
reveal_type(MyModel.objects.all) # N: Revealed type is "def () -> myapp.models.MyQuerySet[myapp.models.MyModel]"
367368
reveal_type(MyModel.objects.custom) # N: Revealed type is "def () -> myapp.models.MyQuerySet"
368369
reveal_type(MyModel.objects.all().filter) # N: Revealed type is "def (*args: Any, **kwargs: Any) -> myapp.models.MyQuerySet[myapp.models.MyModel]"
369370
reveal_type(MyModel.objects.custom().filter) # N: Revealed type is "def (*args: Any, **kwargs: Any) -> myapp.models.MyQuerySet"
370-
reveal_type(MyModel.objects2) # N: Revealed type is "myapp.models.MyModel.MyManagerFromMyQuerySet[myapp.models.MyModel]"
371+
reveal_type(MyModel.objects2) # N: Revealed type is "myapp.models.MyManagerFromMyQuerySet[myapp.models.MyModel]"
372+
reveal_type(MyModel._default_manager) # N: Revealed type is "myapp.models.MyManagerFromMyQuerySet[myapp.models.MyModel]"
371373
installed_apps:
372374
- myapp
373375
files:
@@ -387,6 +389,31 @@
387389
objects = MyManager.from_queryset(MyQuerySet)()
388390
objects2 = MyManager.from_queryset(MyQuerySet)()
389391
392+
- case: test_queryset_in_model_class_body_subclass
393+
main: |
394+
from myapp.models import MyModel
395+
reveal_type(MyModel.objects) # N: Revealed type is "myapp.models.BaseManagerFromBaseQuerySet[myapp.models.MyModel]"
396+
installed_apps:
397+
- myapp
398+
files:
399+
- path: myapp/__init__.py
400+
- path: myapp/models.py
401+
content: |
402+
from django.db import models
403+
404+
class BaseManager(models.Manager["BaseModel"]):
405+
pass
406+
407+
class BaseQuerySet(models.QuerySet["BaseModel"]):
408+
def custom(self) -> "BaseQuerySet":
409+
pass
410+
411+
class BaseModel(models.Model):
412+
objects = BaseManager.from_queryset(BaseQuerySet)()
413+
414+
class MyModel(BaseModel):
415+
pass
416+
390417
- case: from_queryset_includes_methods_returning_queryset
391418
main: |
392419
from myapp.models import MyModel

0 commit comments

Comments
 (0)