Repository: typeddjango/django-stubs

Issue:
Problem with manager created using .from_queryset() and imported from other module
# Bug report

When a manager is created using `.from_queryset()` and imported from another module, it's type is `Any`, but only when the `QuerySet` subclass has its generic specified as `["MyModel"]`.

## What's wrong

I took [this test case](https://github.com/typeddjango/django-stubs/blob/214b0c7439d67b423815515c49eda8ecf0d62bfa/tests/typecheck/managers/querysets/test_from_queryset.yml#L25-L72) and adapted it to move the `NewManager = BaseManager.from_queryset(ModelQuerySet)` line into `myapp/querysets.py` and imported `NewManager` into `myapp/models.py` instead of `ModelQuerySet`.

See this diff: [from_queryset-extra-tests.txt](https://github.com/typeddjango/django-stubs/files/9005556/from_queryset-extra-tests.txt)

Applying this to the current `master` results in the following two tests failing:
- `from_queryset_using_basemanager_with_manager_imported_from_other_module`
- `from_queryset_using_manager_with_manager_imported_from_other_module`

If I change the following in `myapp/querysets.py` for these tests then they pass:

```python
-                class ModelQuerySet(models.QuerySet["MyModel"]):
+                class ModelQuerySet(models.QuerySet):
```

Weird.

As far as I could work out `get_dynamic_class_hook` is being called for `myapp.models.NewManager`, but then `get_function_hook`/`get_method_hook` doesn't seem to get called for methods on the manager in the failure case.  

## How is that should be

When `NewManager` is created using `.from_queryset()` and is imported from another module it shouldn't resolve to `Any` when `Queryset["MyModel"]` is used as the subclass, but should be the same as when only `QuerySet` is used as the subclass or `NewManager` is created in `myapp/models.py`.

## System information

- OS:
- `python` version: 3.9.5
- `django` version: 3.2.13
- `mypy` version: 0.961
- `django-stubs` version: 1.12.0
- `django-stubs-ext` version: 0.5.0



Discussion:
Interestingly enough, the hook is called if I do `class ModelQuerySet(models.QuerySet[models.Model])` as well. 
Hmm. Yes. And it also works if I do `class ModelQuerySet(models.QuerySet["models.Model"])`, so it's not specifically to do with string annotations. Most likely something to do with the circular dependency and the order things are resolved - maybe we need to defer for an extra pass somewhere?
I'm suspecting it has to do with this change: https://github.com/typeddjango/django-stubs/pull/999#discussion_r899525241

Or at least that it takes that path to add the manager. I have a change in #1025 that might resolve it. I could try if that's a correct assumption 

Resolving patch:
diff --git a/mypy_django_plugin/transformers/models.py b/mypy_django_plugin/transformers/models.py
--- a/mypy_django_plugin/transformers/models.py
+++ b/mypy_django_plugin/transformers/models.py
@@ -243,26 +243,25 @@ def run_with_model_cls(self, model_cls: Type[Model]) -> None:
             except helpers.IncompleteDefnException as exc:
                 # Check if manager is a generated (dynamic class) manager
                 base_manager_fullname = helpers.get_class_fullname(manager.__class__.__bases__[0])
-                if manager_fullname not in self.get_generated_manager_mappings(base_manager_fullname):
+                generated_managers = self.get_generated_manager_mappings(base_manager_fullname)
+                if manager_fullname not in generated_managers:
                     # Manager doesn't appear to be generated. Track that we encountered an
                     # incomplete definition and skip
                     incomplete_manager_defs.add(manager_name)
-                continue
+                    continue
+
+                manager_info = self.lookup_typeinfo(generated_managers[manager_fullname])
+                if manager_info is None:
+                    continue
 
-            if manager_name not in self.model_classdef.info.names:
+            is_dynamically_generated = manager_info.metadata.get("django", {}).get("from_queryset_manager") is not None
+            if manager_name not in self.model_classdef.info.names or is_dynamically_generated:
                 manager_type = Instance(manager_info, [Instance(self.model_classdef.info, [])])
                 self.add_new_node_to_model_class(manager_name, manager_type)
-            else:
+            elif self.has_any_parametrized_manager_as_base(manager_info):
                 # Ending up here could for instance be due to having a custom _Manager_
                 # that is not built from a custom QuerySet. Another example is a
                 # related manager.
-                # Don't interfere with dynamically generated manager classes
-                is_dynamically_generated = "django" in manager_info.metadata and manager_info.metadata["django"].get(
-                    "from_queryset_manager"
-                )
-                if not self.has_any_parametrized_manager_as_base(manager_info) or is_dynamically_generated:
-                    continue
-
                 custom_model_manager_name = manager.model.__name__ + "_" + manager_class_name
                 try:
                     custom_manager_type = self.create_new_model_parametrized_manager(


Test patch:
diff --git a/tests/typecheck/managers/querysets/test_from_queryset.yml b/tests/typecheck/managers/querysets/test_from_queryset.yml
--- a/tests/typecheck/managers/querysets/test_from_queryset.yml
+++ b/tests/typecheck/managers/querysets/test_from_queryset.yml
@@ -71,6 +71,55 @@
                 class MyModel(models.Model):
                     objects = NewManager()
 
+-   case: from_queryset_generated_manager_imported_from_other_module
+    main: |
+        from myapp.models import MyModel
+        reveal_type(MyModel.objects)  # N: Revealed type is "myapp.querysets.NewManager[myapp.models.MyModel]"
+        reveal_type(MyModel.objects.get())  # N: Revealed type is "myapp.models.MyModel"
+        reveal_type(MyModel.objects.queryset_method())  # N: Revealed type is "myapp.querysets.ModelQuerySet"
+        reveal_type(MyModel.objects.queryset_method_2())  # N: Revealed type is "typing.Iterable[myapp.querysets.Custom]"
+        reveal_type(MyModel.objects.queryset_method_3())  # N: Revealed type is "builtins.str"
+        reveal_type(MyModel.objects.queryset_method_4([]))  # N: Revealed type is "None"
+        reveal_type(MyModel.objects.filter(id=1).queryset_method()) # N: Revealed type is "myapp.querysets.ModelQuerySet"
+        reveal_type(MyModel.objects.filter(id=1)) # N: Revealed type is "myapp.querysets.ModelQuerySet[myapp.models.MyModel]"
+    installed_apps:
+        - myapp
+    files:
+        -   path: myapp/__init__.py
+        -   path: myapp/querysets.py
+            content: |
+                from typing import TYPE_CHECKING, Iterable, Sequence
+                from django.db import models
+                from django.db.models.manager import BaseManager
+                if TYPE_CHECKING:
+                    from .models import MyModel
+
+                class Custom:
+                    ...
+
+                class ModelQuerySet(models.QuerySet["MyModel"]):
+                    def queryset_method(self) -> "ModelQuerySet":
+                        return self.filter()
+
+                    def queryset_method_2(self) -> Iterable[Custom]:
+                        return []
+
+                    def queryset_method_3(self) -> str:
+                        return 'hello'
+
+                    def queryset_method_4(self, arg: Sequence) -> None:
+                        return None
+
+                NewManager = BaseManager.from_queryset(ModelQuerySet)
+
+        -   path: myapp/models.py
+            content: |
+                from django.db import models
+                from .querysets import NewManager
+
+                class MyModel(models.Model):
+                    objects = NewManager()
+
 -   case: from_queryset_with_manager
     main: |
         from myapp.models import MyModel