Repository: typeddjango/django-stubs

Issue:
ManyToManyField not typed properly when using import alias for related model
# Bug report

<!--
Hi, thanks for submitting a bug. We appreciate that.

But, we will need some information about what's wrong to help you.
-->

## What's wrong

Looks like the transformer for ManyToManyField doesn't handle the case for when the related model passed in is aliased. The `to` model is still typed properly, since it's inferred and returned as the FunctionContext `default_return_type`, but any generated `through` model is never typed.

Example:
```
from django.contrib.auth import models as auth_models
from django.db import models

class MyModel(models.Model):
    allowed_groups = models.ManyToManyField(auth_models.Group)
```

I believe [`get_model_from_expression()`](https://github.com/typeddjango/django-stubs/blob/master/mypy_django_plugin/transformers/manytomany.py#L122) gets passed a MemberExpr as such:
```
MemberExpr:11(
  NameExpr(auth_models [django.contrib.auth.models])
  Group [django.contrib.auth.models.Group])
```

But this case is only handled explicitly for the django.contrib.auth.models.User model. In other cases, it returns None, and the transform for the whole field just returns the default value.


## How is that should be

Since normal RelatedFields resolve their related models fine with aliased imports, it seems the manytomany transformer should be able to use the same logic (a la `django_context.get_field_related_model_cls(current_field)`) for resolving both its `to` and `through` models. Though I'm sure there's some complexities around how Django models m2m relations that would require some changes from how `get_field_related_model_cls` works.

## System information

- OS:
- `python` version: 3.8
- `django` version: 4.2.9
- `mypy` version: 1.10
- `django-stubs` version: 5.0
- `django-stubs-ext` version:



Discussion:
Actually, I think the easiest fix is to change this condition to check for RefExpr instead of NameExpr:
https://github.com/typeddjango/django-stubs/blob/4a5b0656be5ccfb961e4e32f4a99756eaf57bb1d/mypy_django_plugin/transformers/manytomany.py#L132

Resolving patch:
diff --git a/mypy_django_plugin/transformers/manytomany.py b/mypy_django_plugin/transformers/manytomany.py
--- a/mypy_django_plugin/transformers/manytomany.py
+++ b/mypy_django_plugin/transformers/manytomany.py
@@ -1,7 +1,7 @@
 from typing import NamedTuple, Optional, Tuple, Union
 
 from mypy.checker import TypeChecker
-from mypy.nodes import AssignmentStmt, Expression, MemberExpr, NameExpr, StrExpr, TypeInfo
+from mypy.nodes import AssignmentStmt, Expression, MemberExpr, NameExpr, RefExpr, StrExpr, TypeInfo
 from mypy.plugin import FunctionContext, MethodContext
 from mypy.semanal import SemanticAnalyzer
 from mypy.types import Instance, ProperType, TypeVarType, UninhabitedType
@@ -129,7 +129,7 @@ def get_model_from_expression(
     Attempts to resolve an expression to a 'TypeInfo' instance. Any lazy reference
     argument(e.g. "<app_label>.<object_name>") to a Django model is also attempted.
     """
-    if isinstance(expr, NameExpr) and isinstance(expr.node, TypeInfo):
+    if isinstance(expr, RefExpr) and isinstance(expr.node, TypeInfo):
         if helpers.is_model_type(expr.node):
             return Instance(expr.node, [])
 


Test patch:
diff --git a/tests/typecheck/fields/test_related.yml b/tests/typecheck/fields/test_related.yml
--- a/tests/typecheck/fields/test_related.yml
+++ b/tests/typecheck/fields/test_related.yml
@@ -1340,3 +1340,36 @@
 
                 class MyModel(ConcreteParent, AbstractParent):
                     m2m_5 = models.ManyToManyField(Other, related_name="mymodel")
+
+-   case: test_m2m_related_managers_supports_renamed_imports
+    main: |
+        from myapp.models import MyModel
+        from other.models import Other
+        reveal_type(MyModel.m2m_1.through.objects)  # N: Revealed type is "django.db.models.manager.Manager[myapp.models.MyModel_m2m_1]"
+        reveal_type(Other.auto_through.through.objects)  # N: Revealed type is "django.db.models.manager.Manager[myapp.models.MyModel_m2m_1]"
+        reveal_type(MyModel.m2m_2.through.objects)  # N: Revealed type is "django.db.models.manager.Manager[myapp.models.Through]"
+        reveal_type(Other.custom_through.through.objects)  # N: Revealed type is "django.db.models.manager.Manager[myapp.models.Through]"
+    installed_apps:
+        -   other
+        -   myapp
+    files:
+        -   path: other/__init__.py
+        -   path: other/models.py
+            content: |
+                from django.db import models
+
+                class Other(models.Model):
+                    ...
+        -   path: myapp/__init__.py
+        -   path: myapp/models.py
+            content: |
+                from django.db import models
+                from other import models as other_models
+
+                class Through(models.Model):
+                    mymodel = models.ForeignKey("myapp.MyModel", on_delete=models.CASCADE)
+                    other = models.ForeignKey(other_models.Other, on_delete=models.CASCADE)
+
+                class MyModel(models.Model):
+                    m2m_1 = models.ManyToManyField(other_models.Other, related_name="auto_through")
+                    m2m_2 = models.ManyToManyField(other_models.Other, related_name="custom_through", through=Through)