Skip to content

Commit 665f4d8

Browse files
authored
Make related manager inherit from objects of related model (#278)
* related manager inherits from objects of related model * fix test typechecking * lint
1 parent b3ed9e4 commit 665f4d8

File tree

3 files changed

+57
-14
lines changed

3 files changed

+57
-14
lines changed

mypy_django_plugin/transformers/models.py

Lines changed: 32 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from typing import Dict, Optional, Type, cast
1+
from typing import Dict, List, Optional, Type, cast
22

33
from django.db.models.base import Model
44
from django.db.models.fields import DateField, DateTimeField
@@ -58,6 +58,12 @@ def add_new_node_to_model_class(self, name: str, typ: MypyType) -> None:
5858
name=name,
5959
sym_type=typ)
6060

61+
def add_new_class_for_current_module(self, name: str, bases: List[Instance]) -> TypeInfo:
62+
current_module = self.api.modules[self.model_classdef.info.module_name]
63+
new_class_info = helpers.add_new_class_for_module(current_module,
64+
name=name, bases=bases)
65+
return new_class_info
66+
6167
def run(self) -> None:
6268
model_cls = self.django_context.get_model_class_by_fullname(self.model_classdef.fullname)
6369
if model_cls is None:
@@ -164,14 +170,12 @@ def create_new_model_parametrized_manager(self, name: str, base_manager_info: Ty
164170
[Instance(self.model_classdef.info, [])])
165171
bases.append(original_base)
166172

167-
current_module = self.api.modules[self.model_classdef.info.module_name]
168-
custom_manager_info = helpers.add_new_class_for_module(current_module,
169-
name=name, bases=bases)
173+
new_manager_info = self.add_new_class_for_current_module(name, bases)
170174
# copy fields to a new manager
171-
new_cls_def_context = ClassDefContext(cls=custom_manager_info.defn,
175+
new_cls_def_context = ClassDefContext(cls=new_manager_info.defn,
172176
reason=self.ctx.reason,
173177
api=self.api)
174-
custom_manager_type = Instance(custom_manager_info, [Instance(self.model_classdef.info, [])])
178+
custom_manager_type = Instance(new_manager_info, [Instance(self.model_classdef.info, [])])
175179

176180
for name, sym in base_manager_info.names.items():
177181
# replace self type with new class, if copying method
@@ -185,10 +189,10 @@ def create_new_model_parametrized_manager(self, name: str, base_manager_info: Ty
185189
new_sym = sym.copy()
186190
if isinstance(new_sym.node, Var):
187191
new_var = Var(name, type=sym.type)
188-
new_var.info = custom_manager_info
189-
new_var._fullname = custom_manager_info.fullname + '.' + name
192+
new_var.info = new_manager_info
193+
new_var._fullname = new_manager_info.fullname + '.' + name
190194
new_sym.node = new_var
191-
custom_manager_info.names[name] = new_sym
195+
new_manager_info.names[name] = new_sym
192196

193197
return custom_manager_type
194198

@@ -268,15 +272,30 @@ def run_with_model_cls(self, model_cls: Type[Model]) -> None:
268272

269273
if isinstance(relation, (ManyToOneRel, ManyToManyRel)):
270274
try:
271-
manager_info = self.lookup_typeinfo_or_incomplete_defn_error(fullnames.RELATED_MANAGER_CLASS)
275+
related_manager_info = self.lookup_typeinfo_or_incomplete_defn_error(fullnames.RELATED_MANAGER_CLASS) # noqa: E501
276+
if 'objects' not in related_model_info.names:
277+
raise helpers.IncompleteDefnException()
272278
except helpers.IncompleteDefnException as exc:
273279
if not self.api.final_iteration:
274280
raise exc
275281
else:
276282
continue
277-
self.add_new_node_to_model_class(attname,
278-
Instance(manager_info, [Instance(related_model_info, [])]))
279-
continue
283+
284+
# create new RelatedManager subclass
285+
parametrized_related_manager_type = Instance(related_manager_info,
286+
[Instance(related_model_info, [])])
287+
default_manager_type = related_model_info.names['objects'].type
288+
if (default_manager_type is None
289+
or not isinstance(default_manager_type, Instance)
290+
or default_manager_type.type.fullname == fullnames.MANAGER_CLASS_FULLNAME):
291+
self.add_new_node_to_model_class(attname, parametrized_related_manager_type)
292+
continue
293+
294+
name = related_model_cls.__name__ + '_' + 'RelatedManager'
295+
bases = [parametrized_related_manager_type, default_manager_type]
296+
new_related_manager_info = self.add_new_class_for_current_module(name, bases)
297+
298+
self.add_new_node_to_model_class(attname, Instance(new_related_manager_info, []))
280299

281300

282301
class AddExtraFieldMethods(ModelClassInitializer):

scripts/enabled_test_modules.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -248,7 +248,7 @@
248248
re.compile(r'"(HttpRequest|WSGIRequest)" has no attribute'),
249249
],
250250
'many_to_many': [
251-
'(expression has type "List[Article]", variable has type "RelatedManager[Article]"',
251+
'(expression has type "List[Article]", variable has type "Article_RelatedManager2',
252252
'"add" of "RelatedManager" has incompatible type "Article"; expected "Union[Publication, int]"',
253253
],
254254
'many_to_one': [

test-data/typecheck/fields/test_related.yml

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -648,3 +648,27 @@
648648
abstract = True
649649
class User(AbstractUser):
650650
pass
651+
652+
653+
- case: related_manager_is_a_subclass_of_default_manager
654+
main: |
655+
from myapp.models import User
656+
reveal_type(User().orders) # N: Revealed type is 'myapp.models.Order_RelatedManager'
657+
reveal_type(User().orders.get()) # N: Revealed type is 'myapp.models.Order*'
658+
reveal_type(User().orders.manager_method()) # N: Revealed type is 'builtins.int'
659+
installed_apps:
660+
- myapp
661+
files:
662+
- path: myapp/__init__.py
663+
- path: myapp/models.py
664+
content: |
665+
from django.db import models
666+
class User(models.Model):
667+
pass
668+
class OrderManager(models.Manager):
669+
def manager_method(self) -> int:
670+
pass
671+
class Order(models.Model):
672+
objects = OrderManager()
673+
user = models.ForeignKey(to=User, on_delete=models.CASCADE, related_name='orders')
674+

0 commit comments

Comments
 (0)