Repository: typeddjango/django-stubs

Issue:
ModuleNotFoundError using a custom apps directory & path
To make my apps more reusable, I prefer to use an `apps` directory in my project root to house them, with the following structure:
```
--Project/
    -- manage.py
    -- apps/
        -- app1/
            -- app.py
            -- models.py
            -- etc...
    -- etc ...
```

To make life easier, in `manage.py`, I have the following:
```
current_path = os.path.dirname(os.path.abspath(__file__))
sys.path.append(os.path.join(current_path, "apps"))
```

This enables me to use e.g. `from app1 import SomeClass` anywhere in the project, instead of `from apps.app1 import SomeClass`.

However, this is causing `mypy` with `django-stubs` (though not without it) to crash a with `ModuleNotFoundError`:
```
  File "/Users/me/Projects/AwesomeProject/apps/app2/models.py", line 4, in <module>
    from app1.models import SomeModel
ModuleNotFoundError: No module named 'app1'
```

I had a similar issue with PyLint, but was able to solve it with the help of [this StackOverflow page](https://stackoverflow.com/questions/1899436/pylint-unable-to-import-error-how-to-set-pythonpath), taking advatange of an initialisation hook to set the system path in a similar way to above but in `.pylintrc`, like so:
```
[MASTER]
init-hook='
  import os, sys;
  from pylint.config import find_pylintrc;
  sys.path.append(os.path.join(os.path.dirname(find_pylintrc()), 'apps'))'
load-plugins=pylint_django, pylint_celery
...
```

My questions are:
    1) Is there a similar approach I can take?
    2) Otherwise, what are alternative suggestions?

ModuleNotFoundError using a custom apps directory & path
To make my apps more reusable, I prefer to use an `apps` directory in my project root to house them, with the following structure:
```
--Project/
    -- manage.py
    -- apps/
        -- app1/
            -- app.py
            -- models.py
            -- etc...
    -- etc ...
```

To make life easier, in `manage.py`, I have the following:
```
current_path = os.path.dirname(os.path.abspath(__file__))
sys.path.append(os.path.join(current_path, "apps"))
```

This enables me to use e.g. `from app1 import SomeClass` anywhere in the project, instead of `from apps.app1 import SomeClass`.

However, this is causing `mypy` with `django-stubs` (though not without it) to crash a with `ModuleNotFoundError`:
```
  File "/Users/me/Projects/AwesomeProject/apps/app2/models.py", line 4, in <module>
    from app1.models import SomeModel
ModuleNotFoundError: No module named 'app1'
```

I had a similar issue with PyLint, but was able to solve it with the help of [this StackOverflow page](https://stackoverflow.com/questions/1899436/pylint-unable-to-import-error-how-to-set-pythonpath), taking advatange of an initialisation hook to set the system path in a similar way to above but in `.pylintrc`, like so:
```
[MASTER]
init-hook='
  import os, sys;
  from pylint.config import find_pylintrc;
  sys.path.append(os.path.join(os.path.dirname(find_pylintrc()), 'apps'))'
load-plugins=pylint_django, pylint_celery
...
```

My questions are:
    1) Is there a similar approach I can take?
    2) Otherwise, what are alternative suggestions?



Discussion:
I think a workaround is to add the `apps`  folder to your PYTHONPATH :
```
PYTHONPATH=:<path_to_apps_folder> mypy
```

I do this too, using a dedicated apps directory, and as @Atala says, I adjust my PYTHONPATH, which works fine.
This isn't possible when you don't control the calling environment creation such as when mypy is called from the Python extension in VSCode.

Could this module respect the `mypy_path` `mypy.ini` setting and add those paths before trying to import the Django settings file?

https://mypy.readthedocs.io/en/stable/config_file.html#import-discovery
I think a workaround is to add the `apps`  folder to your PYTHONPATH :
```
PYTHONPATH=:<path_to_apps_folder> mypy
```

I do this too, using a dedicated apps directory, and as @Atala says, I adjust my PYTHONPATH, which works fine.
This isn't possible when you don't control the calling environment creation such as when mypy is called from the Python extension in VSCode.

Could this module respect the `mypy_path` `mypy.ini` setting and add those paths before trying to import the Django settings file?

https://mypy.readthedocs.io/en/stable/config_file.html#import-discovery

Resolving patch:
diff --git a/mypy_django_plugin/main.py b/mypy_django_plugin/main.py
--- a/mypy_django_plugin/main.py
+++ b/mypy_django_plugin/main.py
@@ -1,8 +1,10 @@
 import configparser
+import sys
 from functools import partial
 from typing import Callable, Dict, List, NoReturn, Optional, Tuple, cast
 
 from django.db.models.fields.related import RelatedField
+from mypy.modulefinder import mypy_path
 from mypy.nodes import MypyFile, TypeInfo
 from mypy.options import Options
 from mypy.plugin import (
@@ -91,6 +93,10 @@ class NewSemanalDjangoPlugin(Plugin):
     def __init__(self, options: Options) -> None:
         super().__init__(options)
         django_settings_module = extract_django_settings_module(options.config_file)
+        # Add paths from MYPYPATH env var
+        sys.path.extend(mypy_path())
+        # Add paths from mypy_path config option
+        sys.path.extend(options.mypy_path)
         self.django_context = DjangoContext(django_settings_module)
 
     def _get_current_queryset_bases(self) -> Dict[str, int]:


Test patch:
diff --git a/test-data/typecheck/test_config.yml b/test-data/typecheck/test_config.yml
--- a/test-data/typecheck/test_config.yml
+++ b/test-data/typecheck/test_config.yml
@@ -38,3 +38,30 @@
               class MyModel(models.Model):
                   user = models.ForeignKey('auth.User', on_delete=models.CASCADE)
 
+-   case: add_mypy_path_to_package_search
+    main: |
+        import extra_module
+    mypy_config: |
+        [mypy]
+        mypy_path = ./extras
+        [mypy.plugins.django-stubs]
+        django_settings_module = mysettings
+    files:
+        -   path: extras/extra_module.py
+            content: |
+                def extra_fn():
+                  pass
+
+-   case: add_mypypath_env_var_to_package_search
+    main: |
+        import extra_module
+    mypy_config: |
+        [mypy.plugins.django-stubs]
+        django_settings_module = mysettings
+    env:
+      - MYPYPATH=./extras
+    files:
+        -   path: extras/extra_module.py
+            content: |
+                def extra_fn():
+                  pass