From 3212cc20a6028aec1a77ea7b1f7e331d6f819872 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Sun, 10 Mar 2019 00:08:34 +0100 Subject: [PATCH 1/5] Make django_settings_is_configured return the first value always I have been seeing an issue, where it would return False initially, but later True; causing an `AttributeError` with the `mailoutbox` fixture, because `mail.outbox` was not set on the module. This can happen with imported code that changes the environment, e.g. via "wsgi/asgi.py": > os.environ.setdefault("DJANGO_SETTINGS_MODULE", "foo.settings") --- pytest_django/lazy_django.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/pytest_django/lazy_django.py b/pytest_django/lazy_django.py index 9fd715bf9..654413634 100644 --- a/pytest_django/lazy_django.py +++ b/pytest_django/lazy_django.py @@ -8,6 +8,9 @@ import pytest +_django_settings_is_configured = None + + def skip_if_no_django(): """Raises a skip exception when no Django settings are available""" if not django_settings_is_configured(): @@ -21,12 +24,17 @@ def django_settings_is_configured(): configured flag in the Django settings object if django.conf has already been imported. """ - ret = bool(os.environ.get("DJANGO_SETTINGS_MODULE")) + global _django_settings_is_configured + + if _django_settings_is_configured is None: + ret = bool(os.environ.get("DJANGO_SETTINGS_MODULE")) + + if not ret and "django.conf" in sys.modules: + ret = sys.modules["django.conf"].settings.configured - if not ret and "django.conf" in sys.modules: - return sys.modules["django.conf"].settings.configured + _django_settings_is_configured = ret - return ret + return _django_settings_is_configured def get_django_version(): From 72d4f5b56a1b6bbe001bad9491d2e153267a3f60 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Fri, 15 Mar 2019 03:37:09 +0100 Subject: [PATCH 2/5] remove cache, use session-scoped fixture --- pytest_django/lazy_django.py | 16 ++------ pytest_django/plugin.py | 61 +++++++++++++++------------- tests/test_django_settings_module.py | 30 ++++++++++++++ 3 files changed, 66 insertions(+), 41 deletions(-) diff --git a/pytest_django/lazy_django.py b/pytest_django/lazy_django.py index 654413634..e6fc62e34 100644 --- a/pytest_django/lazy_django.py +++ b/pytest_django/lazy_django.py @@ -8,9 +8,6 @@ import pytest -_django_settings_is_configured = None - - def skip_if_no_django(): """Raises a skip exception when no Django settings are available""" if not django_settings_is_configured(): @@ -24,17 +21,12 @@ def django_settings_is_configured(): configured flag in the Django settings object if django.conf has already been imported. """ - global _django_settings_is_configured - - if _django_settings_is_configured is None: - ret = bool(os.environ.get("DJANGO_SETTINGS_MODULE")) - - if not ret and "django.conf" in sys.modules: - ret = sys.modules["django.conf"].settings.configured + ret = bool(os.environ.get("DJANGO_SETTINGS_MODULE")) - _django_settings_is_configured = ret + if not ret and "django.conf" in sys.modules: + ret = sys.modules["django.conf"].settings.configured - return _django_settings_is_configured + return ret def get_django_version(): diff --git a/pytest_django/plugin.py b/pytest_django/plugin.py index 8ac10fb55..badf8e964 100644 --- a/pytest_django/plugin.py +++ b/pytest_django/plugin.py @@ -451,8 +451,13 @@ def get_order_number(test): items[:] = sorted(items, key=get_order_number) +@pytest.fixture(scope="session") +def _django_settings_is_configured(): + return django_settings_is_configured() + + @pytest.fixture(autouse=True, scope="session") -def django_test_environment(request): +def django_test_environment(_django_settings_is_configured): """ Ensure that Django is loaded and has its testing environment setup. @@ -463,18 +468,22 @@ def django_test_environment(request): without duplicating a lot more of Django's test support code we need to follow this model. """ - if django_settings_is_configured(): + if _django_settings_is_configured: _setup_django() from django.conf import settings as dj_settings from django.test.utils import setup_test_environment, teardown_test_environment dj_settings.DEBUG = False setup_test_environment() - request.addfinalizer(teardown_test_environment) + + yield + + if _django_settings_is_configured: + teardown_test_environment() @pytest.fixture(scope="session") -def django_db_blocker(): +def django_db_blocker(_django_settings_is_configured): """Wrapper around Django's database access. This object can be used to re-enable database access. This fixture is used @@ -487,10 +496,8 @@ def django_db_blocker(): This is an advanced feature that is meant to be used to implement database fixtures. """ - if not django_settings_is_configured(): - return None - - return _blocking_manager + if _django_settings_is_configured: + return _blocking_manager @pytest.fixture(autouse=True) @@ -512,9 +519,9 @@ def _django_db_marker(request): @pytest.fixture(autouse=True, scope="class") -def _django_setup_unittest(request, django_db_blocker): +def _django_setup_unittest(request, django_db_blocker, _django_settings_is_configured): """Setup a django unittest, internal to pytest-django.""" - if not django_settings_is_configured() or not is_django_unittest(request): + if not _django_settings_is_configured or not is_django_unittest(request): yield return @@ -553,23 +560,20 @@ def _cleaning_debug(self): @pytest.fixture(scope="function", autouse=True) -def _dj_autoclear_mailbox(): - if not django_settings_is_configured(): - return - - from django.core import mail +def _dj_autoclear_mailbox(_django_settings_is_configured): + if _django_settings_is_configured: + from django.core import mail - del mail.outbox[:] + del mail.outbox[:] @pytest.fixture(scope="function") -def mailoutbox(monkeypatch, django_mail_patch_dns, _dj_autoclear_mailbox): - if not django_settings_is_configured(): - return +def mailoutbox(monkeypatch, django_mail_patch_dns, _dj_autoclear_mailbox, + _django_settings_is_configured): + if _django_settings_is_configured: + from django.core import mail - from django.core import mail - - return mail.outbox + return mail.outbox @pytest.fixture(scope="function") @@ -615,7 +619,7 @@ def restore(): @pytest.fixture(autouse=True, scope="session") -def _fail_for_invalid_template_variable(request): +def _fail_for_invalid_template_variable(_django_settings_is_configured): """Fixture that fails for invalid variables in templates. This fixture will fail each test that uses django template rendering @@ -687,7 +691,7 @@ def __mod__(self, var): if ( os.environ.get(INVALID_TEMPLATE_VARS_ENV, "false") == "true" - and django_settings_is_configured() + and _django_settings_is_configured ): from django.conf import settings as dj_settings @@ -700,12 +704,12 @@ def __mod__(self, var): @pytest.fixture(autouse=True) -def _template_string_if_invalid_marker(request): +def _template_string_if_invalid_marker(request, _django_settings_is_configured): """Apply the @pytest.mark.ignore_template_errors marker, internal to pytest-django.""" marker = request.keywords.get("ignore_template_errors", None) if os.environ.get(INVALID_TEMPLATE_VARS_ENV, "false") == "true": - if marker and django_settings_is_configured(): + if marker and _django_settings_is_configured: from django.conf import settings as dj_settings if dj_settings.TEMPLATES: @@ -715,12 +719,11 @@ def _template_string_if_invalid_marker(request): @pytest.fixture(autouse=True, scope="function") -def _django_clear_site_cache(): +def _django_clear_site_cache(_django_settings_is_configured): """Clears ``django.contrib.sites.models.SITE_CACHE`` to avoid unexpected behavior with cached site objects. """ - - if django_settings_is_configured(): + if _django_settings_is_configured: from django.conf import settings as dj_settings if "django.contrib.sites" in dj_settings.INSTALLED_APPS: diff --git a/tests/test_django_settings_module.py b/tests/test_django_settings_module.py index 09b8427e8..cfc4787c1 100644 --- a/tests/test_django_settings_module.py +++ b/tests/test_django_settings_module.py @@ -450,3 +450,33 @@ def test_no_django_settings_but_django_imported(testdir, monkeypatch): testdir.makeconftest("import django") r = testdir.runpytest_subprocess("--help") assert r.ret == 0 + + +def test_django_settings_is_configured_cached(testdir, monkeypatch): + """ + ``django_settings_is_configured`` should return the initial value always. + + This avoids having the _dj_autoclear_mailbox autouse fixture trigger + "AttributeError: module 'django.core.mail' has no attribute 'outbox'". + """ + monkeypatch.delenv("DJANGO_SETTINGS_MODULE") + + p = testdir.makepyfile( + """ + from pytest_django.lazy_django import django_settings_is_configured + + def test_1(_django_settings_is_configured): + import os + + assert not _django_settings_is_configured + assert not django_settings_is_configured() + + os.environ["DJANGO_SETTINGS_MODULE"] = "ignored_dsm" + + def test_2(_django_settings_is_configured): + assert not _django_settings_is_configured + assert django_settings_is_configured() + """) + result = testdir.runpytest_subprocess(p, "-s") + result.stdout.fnmatch_lines(["*2 passed in*"]) + assert result.ret == 0 From 33282a81311020af5fc276eb39a4532c8478413c Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Fri, 15 Mar 2019 03:48:11 +0100 Subject: [PATCH 3/5] remove unused fixture args --- pytest_django/plugin.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pytest_django/plugin.py b/pytest_django/plugin.py index badf8e964..4d8c87366 100644 --- a/pytest_django/plugin.py +++ b/pytest_django/plugin.py @@ -568,7 +568,7 @@ def _dj_autoclear_mailbox(_django_settings_is_configured): @pytest.fixture(scope="function") -def mailoutbox(monkeypatch, django_mail_patch_dns, _dj_autoclear_mailbox, +def mailoutbox(django_mail_patch_dns, _dj_autoclear_mailbox, _django_settings_is_configured): if _django_settings_is_configured: from django.core import mail @@ -584,7 +584,7 @@ def django_mail_patch_dns(monkeypatch, django_mail_dnsname): @pytest.fixture(scope="function") -def django_mail_dnsname(monkeypatch): +def django_mail_dnsname(): return "fake-tests.example.com" From 1161f104e6025432e3afe90d0d8158c4e250e345 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Fri, 15 Mar 2019 03:49:03 +0100 Subject: [PATCH 4/5] django_mail_patch_dns: check _django_settings_is_configured: --- pytest_django/plugin.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/pytest_django/plugin.py b/pytest_django/plugin.py index 4d8c87366..48bf8ae0c 100644 --- a/pytest_django/plugin.py +++ b/pytest_django/plugin.py @@ -577,10 +577,12 @@ def mailoutbox(django_mail_patch_dns, _dj_autoclear_mailbox, @pytest.fixture(scope="function") -def django_mail_patch_dns(monkeypatch, django_mail_dnsname): - from django.core import mail +def django_mail_patch_dns(monkeypatch, django_mail_dnsname, + _django_settings_is_configured): + if _django_settings_is_configured: + from django.core import mail - monkeypatch.setattr(mail.message, "DNS_NAME", django_mail_dnsname) + monkeypatch.setattr(mail.message, "DNS_NAME", django_mail_dnsname) @pytest.fixture(scope="function") From 74e310a7cf88338c9a9c05c05c835e0aecca27b4 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Fri, 15 Mar 2019 03:51:54 +0100 Subject: [PATCH 5/5] doc --- tests/test_django_settings_module.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/tests/test_django_settings_module.py b/tests/test_django_settings_module.py index cfc4787c1..88cf2c6d1 100644 --- a/tests/test_django_settings_module.py +++ b/tests/test_django_settings_module.py @@ -452,12 +452,14 @@ def test_no_django_settings_but_django_imported(testdir, monkeypatch): assert r.ret == 0 -def test_django_settings_is_configured_cached(testdir, monkeypatch): +def test_django_settings_is_configured_first(testdir, monkeypatch): """ - ``django_settings_is_configured`` should return the initial value always. + The value from ``django_settings_is_configured`` should be used internally + always. This avoids having the _dj_autoclear_mailbox autouse fixture trigger - "AttributeError: module 'django.core.mail' has no attribute 'outbox'". + "AttributeError: module 'django.core.mail' has no attribute 'outbox'", + because Django / the test environment was not setup initially. """ monkeypatch.delenv("DJANGO_SETTINGS_MODULE")