Repository: typeddjango/django-stubs

Issue:
Allow setting `django_settings_module` from the `DJANGO_SETTINGS_MODULE` env variable
Hi there!

Hope everyone is doing alright! I have a use-case where I deploy my Django application with different `INSTALLED_APPS` in two different deployments. The least invasive way I found to do this was having two separate settings files and switching between them based on whichever one I am deploying. Things like `pytest` allow the setting of `django_settings_module` from both the environment and the `pyproject.toml`. It seems like `django_stubs` wants the settings to be static which doesn't really make sense to me considering Django has deemed it as an env variable for that flexibility. 

Let me know if I am missing something, or if there is a workaround/fix that would be easier.

Thanks!


Discussion:
We can try this, PR is welcome.

Resolving patch:
diff --git a/mypy_django_plugin/config.py b/mypy_django_plugin/config.py
--- a/mypy_django_plugin/config.py
+++ b/mypy_django_plugin/config.py
@@ -1,4 +1,5 @@
 import configparser
+import os
 import sys
 import textwrap
 from functools import partial
@@ -14,7 +15,7 @@
 (config)
 ...
 [mypy.plugins.django-stubs]
-django_settings_module = str (required)
+django_settings_module = str (default: `os.getenv("DJANGO_SETTINGS_MODULE")`)
 strict_settings = bool (default: true)
 ...
 """
@@ -22,14 +23,16 @@
 (config)
 ...
 [tool.django-stubs]
-django_settings_module = str (required)
+django_settings_module = str (default: `os.getenv("DJANGO_SETTINGS_MODULE")`)
 strict_settings = bool (default: true)
 ...
 """
 INVALID_FILE = "mypy config file is not specified or found"
 COULD_NOT_LOAD_FILE = "could not load configuration file"
 MISSING_SECTION = "no section [{section}] found"
-MISSING_DJANGO_SETTINGS = "missing required 'django_settings_module' config"
+DJANGO_SETTINGS_ENV_VAR = "DJANGO_SETTINGS_MODULE"
+MISSING_DJANGO_SETTINGS = f"missing required 'django_settings_module' config.\
+ Either specify this config or set your `{DJANGO_SETTINGS_ENV_VAR}` env var"
 INVALID_BOOL_SETTING = "invalid {key!r}: the setting must be a boolean"
 
 
@@ -80,10 +83,12 @@ def parse_toml_file(self, filepath: Path) -> None:
         except KeyError:
             toml_exit(MISSING_SECTION.format(section="tool.django-stubs"))
 
-        if "django_settings_module" not in config:
+        django_settings_module = config.get("django_settings_module") or os.getenv(DJANGO_SETTINGS_ENV_VAR)
+        if not django_settings_module:
             toml_exit(MISSING_DJANGO_SETTINGS)
 
-        self.django_settings_module = config["django_settings_module"]
+        self.django_settings_module = django_settings_module
+
         if not isinstance(self.django_settings_module, str):
             toml_exit("invalid 'django_settings_module': the setting must be a string")
 
@@ -103,10 +108,15 @@ def parse_ini_file(self, filepath: Path) -> None:
         if not parser.has_section(section):
             exit_with_error(MISSING_SECTION.format(section=section))
 
-        if not parser.has_option(section, "django_settings_module"):
+        if parser.has_option(section, "django_settings_module"):
+            django_settings_module = parser.get(section, "django_settings_module").strip("'\"")
+        else:
+            django_settings_module = os.getenv(DJANGO_SETTINGS_ENV_VAR, "")
+
+        if not django_settings_module:
             exit_with_error(MISSING_DJANGO_SETTINGS)
 
-        self.django_settings_module = parser.get(section, "django_settings_module").strip("'\"")
+        self.django_settings_module = django_settings_module
 
         try:
             self.strict_settings = parser.getboolean(section, "strict_settings", fallback=True)


Test patch:
diff --git a/tests/test_error_handling.py b/tests/test_error_handling.py
--- a/tests/test_error_handling.py
+++ b/tests/test_error_handling.py
@@ -1,7 +1,9 @@
+import os
 import tempfile
 import uuid
 from contextlib import contextmanager
 from typing import Any, Generator, List, Optional
+from unittest import mock
 
 import pytest
 
@@ -11,7 +13,7 @@
 (config)
 ...
 [mypy.plugins.django-stubs]
-django_settings_module = str (required)
+django_settings_module = str (default: `os.getenv("DJANGO_SETTINGS_MODULE")`)
 strict_settings = bool (default: true)
 ...
 (django-stubs) mypy: error: {}
@@ -21,7 +23,7 @@
 (config)
 ...
 [tool.django-stubs]
-django_settings_module = str (required)
+django_settings_module = str (default: `os.getenv("DJANGO_SETTINGS_MODULE")`)
 strict_settings = bool (default: true)
 ...
 (django-stubs) mypy: error: {}
@@ -46,12 +48,14 @@ def write_to_file(file_contents: str, suffix: Optional[str] = None) -> Generator
         ),
         pytest.param(
             ["[mypy.plugins.django-stubs]", "\tnot_django_not_settings_module = badbadmodule"],
-            "missing required 'django_settings_module' config",
+            "missing required 'django_settings_module' config.\
+ Either specify this config or set your `DJANGO_SETTINGS_MODULE` env var",
             id="missing-settings-module",
         ),
         pytest.param(
             ["[mypy.plugins.django-stubs]"],
-            "missing required 'django_settings_module' config",
+            "missing required 'django_settings_module' config.\
+ Either specify this config or set your `DJANGO_SETTINGS_MODULE` env var",
             id="no-settings-given",
         ),
         pytest.param(
@@ -112,7 +116,8 @@ def test_handles_filename(capsys: Any, filename: str) -> None:
             [tool.django-stubs]
             not_django_not_settings_module = "badbadmodule"
             """,
-            "missing required 'django_settings_module' config",
+            "missing required 'django_settings_module' config.\
+ Either specify this config or set your `DJANGO_SETTINGS_MODULE` env var",
             id="missing django_settings_module",
         ),
         pytest.param(
@@ -172,3 +177,40 @@ def test_correct_configuration(boolean_value) -> None:
 
     assert config.django_settings_module == "my.module"
     assert config.strict_settings is (boolean_value.lower() == "true")
+
+
+@pytest.mark.parametrize("boolean_value", ["true", "false"])
+def test_correct_toml_configuration_with_django_setting_from_env(boolean_value: str) -> None:
+    config_file_contents = f"""
+    [tool.django-stubs]
+    some_other_setting = "setting"
+    strict_settings = {boolean_value}
+    """
+    django_settings_env_value = "my.module"
+
+    with write_to_file(config_file_contents, suffix=".toml") as filename:
+        with mock.patch.dict(os.environ, {"DJANGO_SETTINGS_MODULE": django_settings_env_value}):
+            config = DjangoPluginConfig(filename)
+
+    assert config.django_settings_module == django_settings_env_value
+    assert config.strict_settings is (boolean_value == "true")
+
+
+@pytest.mark.parametrize("boolean_value", ["true", "True", "false", "False"])
+def test_correct_configuration_with_django_setting_from_env(boolean_value) -> None:
+    """Django settings module gets extracted given valid configuration."""
+    config_file_contents = "\n".join(
+        [
+            "[mypy.plugins.django-stubs]",
+            "some_other_setting = setting",
+            f"strict_settings = {boolean_value}",
+        ]
+    )
+    django_settings_env_value = "my.module"
+
+    with write_to_file(config_file_contents) as filename:
+        with mock.patch.dict(os.environ, {"DJANGO_SETTINGS_MODULE": django_settings_env_value}):
+            config = DjangoPluginConfig(filename)
+
+    assert config.django_settings_module == django_settings_env_value
+    assert config.strict_settings is (boolean_value.lower() == "true")