From 569587d1c53d925de0832b392711c07fe06ac16d Mon Sep 17 00:00:00 2001
From: julian <j.feinauer@pragmaticminds.de>
Date: Mon, 21 Mar 2022 13:35:05 +0100
Subject: [PATCH 1/5] add DJANGO_CONFIGURATION_HOOK as optional parameter to
 allow customly configured settings instances

---
 pytest_django/plugin.py              | 41 ++++++++++++++++++++++++++++
 tests/test_django_settings_module.py | 29 ++++++++++++++++++++
 2 files changed, 70 insertions(+)

diff --git a/pytest_django/plugin.py b/pytest_django/plugin.py
index e2b6f120..e86b2d8f 100644
--- a/pytest_django/plugin.py
+++ b/pytest_django/plugin.py
@@ -55,6 +55,7 @@
 
 SETTINGS_MODULE_ENV = "DJANGO_SETTINGS_MODULE"
 CONFIGURATION_ENV = "DJANGO_CONFIGURATION"
+CONFIGURATION_HOOK_ENV = "DJANGO_CONFIGURATION_HOOK"
 INVALID_TEMPLATE_VARS_ENV = "FAIL_INVALID_TEMPLATE_VARS"
 
 _report_header = []
@@ -98,6 +99,14 @@ def pytest_addoption(parser) -> None:
         default=None,
         help="Set DJANGO_CONFIGURATION.",
     )
+    group.addoption(
+        "--dch",
+        action="store",
+        type=str,
+        dest="dch",
+        default=None,
+        help="Set DJANGO_CONFIGURATION_HOOK.",
+    )
     group.addoption(
         "--nomigrations",
         "--no-migrations",
@@ -124,6 +133,9 @@ def pytest_addoption(parser) -> None:
     parser.addini(
         SETTINGS_MODULE_ENV, "Django settings module to use by pytest-django."
     )
+    parser.addini(
+        CONFIGURATION_HOOK_ENV, "Callback Hook to prepare Django settings alternatively."
+    )
 
     parser.addini(
         "django_find_project",
@@ -329,6 +341,7 @@ def _get_option_with_source(
 
     ds, ds_source = _get_option_with_source(options.ds, SETTINGS_MODULE_ENV)
     dc, dc_source = _get_option_with_source(options.dc, CONFIGURATION_ENV)
+    dch, dch_source = _get_option_with_source(options.dch, CONFIGURATION_HOOK_ENV)
 
     if ds:
         _report_header.append(f"settings: {ds} (from {ds_source})")
@@ -349,6 +362,34 @@ def _get_option_with_source(
 
         with _handle_import_error(_django_project_scan_outcome):
             dj_settings.DATABASES
+    elif dch:
+        # Forcefully load Django settings, throws ImportError or
+        # ImproperlyConfigured if settings cannot be loaded.
+        from django.conf import settings as dj_settings
+
+        # Call a HOOK that could initialize djangos
+        # object with a custom configuration
+
+        if "." not in dch:
+            raise ImportError(f"Invalid path for configuration hook: {dch}")
+
+        pkg_parts = dch.split(".")
+        module_path = ".".join(pkg_parts[:-1])
+        function_name = pkg_parts[-1]
+
+        import importlib
+
+        try:
+            mod = importlib.import_module(module_path)
+        except (ImportError, AttributeError):
+            raise ImportError(f"Unable to import module {module_path}")
+        func = getattr(mod, function_name, None)
+
+        if not func:
+            raise ImportError(f"No function found with name {function_name} in module {module_path}!")
+
+        # Call the function
+        func()
 
     _setup_django()
 
diff --git a/tests/test_django_settings_module.py b/tests/test_django_settings_module.py
index fb008e12..b4b49305 100644
--- a/tests/test_django_settings_module.py
+++ b/tests/test_django_settings_module.py
@@ -505,3 +505,32 @@ def test_no_django_settings_but_django_imported(testdir, monkeypatch) -> None:
     testdir.makeconftest("import django")
     r = testdir.runpytest_subprocess("--help")
     assert r.ret == 0
+
+
+def test_dch_ini(testdir, monkeypatch) -> None:
+    monkeypatch.delenv("DJANGO_SETTINGS_MODULE")
+    testdir.makeini(
+        """
+       [pytest]
+       DJANGO_CONFIGURATION_HOOK = tpkg.test.setup
+    """
+    )
+    pkg = testdir.mkpydir("tpkg")
+    pkg.join("test.py").write(
+        """
+# Test
+from django.conf import settings
+
+def setup():
+  settings.configure() 
+""")
+    testdir.makepyfile(
+        """
+        import os
+
+        def test_ds():
+            pass
+    """
+    )
+    result = testdir.runpytest_subprocess()
+    assert result.ret == 0

From 0b9dad148facf334214b517c8fedf3903f452b1c Mon Sep 17 00:00:00 2001
From: julian <j.feinauer@pragmaticminds.de>
Date: Mon, 21 Mar 2022 13:47:05 +0100
Subject: [PATCH 2/5] fixed linting

---
 pytest_django/plugin.py              | 3 ++-
 tests/test_django_settings_module.py | 5 ++---
 2 files changed, 4 insertions(+), 4 deletions(-)

diff --git a/pytest_django/plugin.py b/pytest_django/plugin.py
index e86b2d8f..5c9b64bf 100644
--- a/pytest_django/plugin.py
+++ b/pytest_django/plugin.py
@@ -386,7 +386,8 @@ def _get_option_with_source(
         func = getattr(mod, function_name, None)
 
         if not func:
-            raise ImportError(f"No function found with name {function_name} in module {module_path}!")
+            raise ImportError(f"No function found with name {function_name} in module "
+                              f"{module_path}!")
 
         # Call the function
         func()
diff --git a/tests/test_django_settings_module.py b/tests/test_django_settings_module.py
index b4b49305..f786ba7b 100644
--- a/tests/test_django_settings_module.py
+++ b/tests/test_django_settings_module.py
@@ -516,13 +516,12 @@ def test_dch_ini(testdir, monkeypatch) -> None:
     """
     )
     pkg = testdir.mkpydir("tpkg")
-    pkg.join("test.py").write(
-        """
+    pkg.join("test.py").write("""
 # Test
 from django.conf import settings
 
 def setup():
-  settings.configure() 
+  settings.configure()
 """)
     testdir.makepyfile(
         """

From 5f9fa9916310d726de5e006b0c5fcc5d77563c4e Mon Sep 17 00:00:00 2001
From: julian <j.feinauer@pragmaticminds.de>
Date: Mon, 21 Mar 2022 13:56:53 +0100
Subject: [PATCH 3/5] removed f-strings

---
 pytest_django/plugin.py | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/pytest_django/plugin.py b/pytest_django/plugin.py
index 5c9b64bf..6f76d3d0 100644
--- a/pytest_django/plugin.py
+++ b/pytest_django/plugin.py
@@ -371,7 +371,7 @@ def _get_option_with_source(
         # object with a custom configuration
 
         if "." not in dch:
-            raise ImportError(f"Invalid path for configuration hook: {dch}")
+            raise ImportError("Invalid path for configuration hook: {}".format(dch))
 
         pkg_parts = dch.split(".")
         module_path = ".".join(pkg_parts[:-1])
@@ -382,12 +382,12 @@ def _get_option_with_source(
         try:
             mod = importlib.import_module(module_path)
         except (ImportError, AttributeError):
-            raise ImportError(f"Unable to import module {module_path}")
+            raise ImportError("Unable to import module {}".format(module_path))
         func = getattr(mod, function_name, None)
 
         if not func:
-            raise ImportError(f"No function found with name {function_name} in module "
-                              f"{module_path}!")
+            raise ImportError("No function found with name {} in module {}!"
+                              .format(function_name, module_path))
 
         # Call the function
         func()

From 5c851a7bd96c8f04c8756a4d6f03c072cf9477f7 Mon Sep 17 00:00:00 2001
From: julian <j.feinauer@pragmaticminds.de>
Date: Mon, 21 Mar 2022 14:18:24 +0100
Subject: [PATCH 4/5] Improved code coverage

---
 tests/test_django_settings_module.py | 51 ++++++++++++++++++++++++++++
 1 file changed, 51 insertions(+)

diff --git a/tests/test_django_settings_module.py b/tests/test_django_settings_module.py
index f786ba7b..c8b8e2c1 100644
--- a/tests/test_django_settings_module.py
+++ b/tests/test_django_settings_module.py
@@ -533,3 +533,54 @@ def test_ds():
     )
     result = testdir.runpytest_subprocess()
     assert result.ret == 0
+
+
+def test_dch_ini_no_module(testdir, monkeypatch) -> None:
+    monkeypatch.delenv("DJANGO_SETTINGS_MODULE")
+    testdir.makeini(
+        """
+       [pytest]
+       DJANGO_CONFIGURATION_HOOK = tpkg.not_existing.setup
+    """
+    )
+    testdir.makepyfile(
+        """
+        import os
+
+        def test_ds():
+            pass
+    """
+    )
+    result = testdir.runpytest_subprocess()
+    result.stderr.fnmatch_lines(["ImportError: Unable to import module tpkg.not_existing"])
+    assert result.ret == 1
+
+
+def test_dch_ini_module_but_no_func(testdir, monkeypatch) -> None:
+    monkeypatch.delenv("DJANGO_SETTINGS_MODULE")
+    testdir.makeini(
+        """
+       [pytest]
+       DJANGO_CONFIGURATION_HOOK = tpkg.test.not_existing_function
+    """
+    )
+    pkg = testdir.mkpydir("tpkg")
+    pkg.join("test.py").write("""
+# Test
+from django.conf import settings
+
+def setup():
+  settings.configure()
+""")
+    testdir.makepyfile(
+        """
+        import os
+
+        def test_ds():
+            pass
+    """
+    )
+    result = testdir.runpytest_subprocess()
+    result.stderr.fnmatch_lines(["ImportError: No function found with name "
+                                 "not_existing_function in module tpkg.test!"])
+    assert result.ret == 1

From 16db732dd2d0e28d271d465b29f7605d85e5f3b8 Mon Sep 17 00:00:00 2001
From: julian <j.feinauer@pragmaticminds.de>
Date: Mon, 21 Mar 2022 14:23:35 +0100
Subject: [PATCH 5/5] Improved code coverage

---
 tests/test_django_settings_module.py | 21 +++++++++++++++++++++
 1 file changed, 21 insertions(+)

diff --git a/tests/test_django_settings_module.py b/tests/test_django_settings_module.py
index c8b8e2c1..641a8528 100644
--- a/tests/test_django_settings_module.py
+++ b/tests/test_django_settings_module.py
@@ -535,6 +535,27 @@ def test_ds():
     assert result.ret == 0
 
 
+def test_dch_ini_invalid_path(testdir, monkeypatch) -> None:
+    monkeypatch.delenv("DJANGO_SETTINGS_MODULE")
+    testdir.makeini(
+        """
+       [pytest]
+       DJANGO_CONFIGURATION_HOOK = invalid_path
+    """
+    )
+    testdir.makepyfile(
+        """
+        import os
+
+        def test_ds():
+            pass
+    """
+    )
+    result = testdir.runpytest_subprocess()
+    result.stderr.fnmatch_lines(["ImportError: Invalid path for configuration hook: invalid_path"])
+    assert result.ret == 1
+
+
 def test_dch_ini_no_module(testdir, monkeypatch) -> None:
     monkeypatch.delenv("DJANGO_SETTINGS_MODULE")
     testdir.makeini(