From b03ea36c75ebf1d83f1e863ebdf76ce236d800c6 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Sun, 21 Apr 2019 11:34:40 +0200 Subject: [PATCH 1/5] tests/conftest.py: move import of db_helpers This is required for when conftests are loaded before Django gets setup (i.e. when it would be done in pytest_configure only). Not necessary, but easier to improve things from there. --- tests/conftest.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index c770d7510..8b76aba29 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -6,8 +6,6 @@ import six from django.conf import settings -from pytest_django_test.db_helpers import DB_NAME, TEST_DB_NAME - try: import pathlib except ImportError: @@ -40,6 +38,8 @@ def testdir(testdir, monkeypatch): @pytest.fixture(scope="function") def django_testdir(request, testdir, monkeypatch): + from pytest_django_test.db_helpers import DB_NAME, TEST_DB_NAME + marker = request.node.get_closest_marker("django_project") options = _marker_apifun(**(marker.kwargs if marker else {})) From 49b835c84741918a7a8ae3481846dbc45008be3d Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Sun, 21 Apr 2019 12:12:11 +0200 Subject: [PATCH 2/5] Setup Django later in pytest_configure --- pytest_django/plugin.py | 97 ++++++++++++++++++----------------------- 1 file changed, 42 insertions(+), 55 deletions(-) diff --git a/pytest_django/plugin.py b/pytest_django/plugin.py index 09c61c801..178a031eb 100644 --- a/pytest_django/plugin.py +++ b/pytest_django/plugin.py @@ -4,7 +4,6 @@ test database and provides some useful text fixtures. """ -import contextlib import inspect from functools import reduce import os @@ -158,16 +157,7 @@ def pytest_addoption(parser): "projects since it is disabled in the configuration " '("django_find_project = false")' ) - - -@contextlib.contextmanager -def _handle_import_error(extra_message): - try: - yield - except ImportError as e: - django_msg = (e.args[0] + "\n\n") if e.args else "" - msg = django_msg + extra_message - raise ImportError(msg) +_django_project_scan_outcome = PROJECT_SCAN_DISABLED def _add_django_project_to_path(args): @@ -262,88 +252,85 @@ def pytest_load_initial_conftests(early_config, parser, args): "variables (if --fail-on-template-vars is used).", ) - options = parser.parse_known_args(args) - - if options.version or options.help: - return - django_find_project = _get_boolean_value( early_config.getini("django_find_project"), "django_find_project" ) - if django_find_project: + global _django_project_scan_outcome _django_project_scan_outcome = _add_django_project_to_path(args) - else: - _django_project_scan_outcome = PROJECT_SCAN_DISABLED + + +def pytest_report_header(config): + if config._dsm_report_header: + return [config._dsm_report_header] + + +@pytest.mark.tryfirst +def pytest_configure(config): + if config.option.version or config.option.help: + return + + # Allow Django settings to be configured in a user's pytest_configure, or + # conftest, but make sure we call django.setup(). if ( - options.itv + config.option.itv or _get_boolean_value( os.environ.get(INVALID_TEMPLATE_VARS_ENV), INVALID_TEMPLATE_VARS_ENV ) - or early_config.getini(INVALID_TEMPLATE_VARS_ENV) + or config.getini(INVALID_TEMPLATE_VARS_ENV) ): os.environ[INVALID_TEMPLATE_VARS_ENV] = "true" # Configure DJANGO_SETTINGS_MODULE - if options.ds: + if config.option.ds: ds_source = "command line option" - ds = options.ds + ds = config.option.ds elif SETTINGS_MODULE_ENV in os.environ: ds = os.environ[SETTINGS_MODULE_ENV] ds_source = "environment variable" - elif early_config.getini(SETTINGS_MODULE_ENV): - ds = early_config.getini(SETTINGS_MODULE_ENV) + elif config.getini(SETTINGS_MODULE_ENV): + ds = config.getini(SETTINGS_MODULE_ENV) ds_source = "ini file" else: ds = None ds_source = None if ds: - early_config._dsm_report_header = "Django settings: %s (from %s)" % ( + config._dsm_report_header = "Django settings: %s (from %s)" % ( ds, ds_source, ) else: - early_config._dsm_report_header = None + config._dsm_report_header = None + return + + os.environ[SETTINGS_MODULE_ENV] = ds # Configure DJANGO_CONFIGURATION dc = ( - options.dc + config.option.dc or os.environ.get(CONFIGURATION_ENV) - or early_config.getini(CONFIGURATION_ENV) + or config.getini(CONFIGURATION_ENV) ) + if dc: + os.environ[CONFIGURATION_ENV] = dc - if ds: - os.environ[SETTINGS_MODULE_ENV] = ds - - if dc: - os.environ[CONFIGURATION_ENV] = dc - - # Install the django-configurations importer - import configurations.importer - - configurations.importer.install() - - # Forcefully load Django settings, throws ImportError or - # ImproperlyConfigured if settings cannot be loaded. - from django.conf import settings as dj_settings - - with _handle_import_error(_django_project_scan_outcome): - dj_settings.DATABASES + # Install the django-configurations importer + import configurations.importer - _setup_django() + configurations.importer.install() + # Forcefully load Django settings. + # Throws ImportError or ImproperlyConfigured if settings cannot be loaded. + from django.conf import settings as dj_settings -def pytest_report_header(config): - if config._dsm_report_header: - return [config._dsm_report_header] - + try: + dj_settings.DATABASES + except ImportError as e: + django_msg = (e.args[0] + "\n\n") if e.args else "" + msg = django_msg + _django_project_scan_outcome -@pytest.mark.trylast -def pytest_configure(): - # Allow Django settings to be configured in a user pytest_configure call, - # but make sure we call django.setup() _setup_django() From c23825942007665d34e4fa6f5a3334d505d37a8f Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Sun, 21 Apr 2019 12:13:46 +0200 Subject: [PATCH 3/5] tests --- tests/test_database.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/test_database.py b/tests/test_database.py index 607aadf47..0e5ad2edb 100644 --- a/tests/test_database.py +++ b/tests/test_database.py @@ -274,8 +274,7 @@ def test_db_access_in_conftest(self, django_testdir): result = django_testdir.runpytest_subprocess("-v") result.stderr.fnmatch_lines( [ - '*Failed: Database access not allowed, use the "django_db" mark, ' - 'or the "db" or "transactional_db" fixtures to enable it.*' + "E django.core.exceptions.AppRegistryNotReady: Apps aren't loaded yet.", ] ) From 251439b157b45926bcaaeed76ce79d1ca6451633 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Sun, 21 Apr 2019 12:40:29 +0200 Subject: [PATCH 4/5] wip --- pytest_django/plugin.py | 18 ++++---- tests/test_initialization.py | 80 ++++++++++++++++++++++++++---------- 2 files changed, 70 insertions(+), 28 deletions(-) diff --git a/pytest_django/plugin.py b/pytest_django/plugin.py index 178a031eb..e1df02908 100644 --- a/pytest_django/plugin.py +++ b/pytest_django/plugin.py @@ -265,7 +265,7 @@ def pytest_report_header(config): return [config._dsm_report_header] -@pytest.mark.tryfirst +@pytest.mark.trylast def pytest_configure(config): if config.option.version or config.option.help: return @@ -296,15 +296,19 @@ def pytest_configure(config): ds = None ds_source = None - if ds: - config._dsm_report_header = "Django settings: %s (from %s)" % ( - ds, - ds_source, - ) - else: + if not ds: config._dsm_report_header = None + + # Setup Django if settings are configured manually. + _setup_django() + return + config._dsm_report_header = "Django settings: %s (from %s)" % ( + ds, + ds_source, + ) + os.environ[SETTINGS_MODULE_ENV] = ds # Configure DJANGO_CONFIGURATION diff --git a/tests/test_initialization.py b/tests/test_initialization.py index 47680e942..1b3e52b84 100644 --- a/tests/test_initialization.py +++ b/tests/test_initialization.py @@ -1,24 +1,40 @@ from textwrap import dedent +import pytest -def test_django_setup_order_and_uniqueness(django_testdir, monkeypatch): + +@pytest.mark.parametrize("conftest_mark", ("tryfirst", "trylast")) +def test_django_setup_order_and_uniqueness(conftest_mark, django_testdir, monkeypatch): """ The django.setup() function shall not be called multiple times by pytest-django, since it resets logging conf each time. """ django_testdir.makeconftest( """ + import pytest import django.apps - assert django.apps.apps.ready - from tpkg.app.models import Item print("conftest") + + assert not django.apps.apps.ready + + orig_django_setup = django.setup + def django_setup(*args, **kwargs): + print("django_setup") + orig_django_setup(*args, **kwargs) + django.setup = django_setup + + @pytest.mark.{conftest_mark} def pytest_configure(): - import django + if "{conftest_mark}" == "tryfirst": + assert not django.apps.apps.ready + else: + assert django.apps.apps.ready + print("pytest_configure: conftest") - django.setup = lambda: SHOULD_NOT_GET_CALLED - """ - ) + """.format( + conftest_mark=conftest_mark + )) django_testdir.project_root.join("tpkg", "plugin.py").write( dedent( @@ -29,13 +45,17 @@ def pytest_configure(): print("plugin") def pytest_configure(): - assert django.apps.apps.ready - from tpkg.app.models import Item + assert not django.apps.apps.ready print("pytest_configure: plugin") - @pytest.hookimpl(tryfirst=True) + @pytest.hookimpl(hookwrapper=True, tryfirst=True) def pytest_load_initial_conftests(early_config, parser, args): - print("pytest_load_initial_conftests") + print("pytest_load_initial_conftests_start") + assert not django.apps.apps.ready + + yield + + print("pytest_load_initial_conftests_end") assert not django.apps.apps.ready """ ) @@ -47,14 +67,32 @@ def test_ds(): """ ) result = django_testdir.runpytest_subprocess("-s", "-p", "tpkg.plugin") - result.stdout.fnmatch_lines( - [ - "plugin", - "pytest_load_initial_conftests", - "conftest", - "pytest_configure: conftest", - "pytest_configure: plugin", - "*1 passed*", - ] - ) + + if conftest_mark == "tryfirst": + result.stdout.fnmatch_lines( + [ + "plugin", + "pytest_load_initial_conftests_start", + "conftest", + "pytest_load_initial_conftests_end", + "pytest_configure: conftest", + "pytest_configure: plugin", + "django_setup", + "*1 passed*", + ] + ) + else: + result.stdout.fnmatch_lines( + [ + "plugin", + "pytest_load_initial_conftests_start", + "conftest", + "pytest_load_initial_conftests_end", + "pytest_configure: plugin", + "django_setup", + "pytest_configure: conftest", + "*1 passed*", + ] + ) + assert result.ret == 0 From 9d6703858f43b539304b331a0ea6e99c744c6bf3 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Sun, 21 Apr 2019 12:50:38 +0200 Subject: [PATCH 5/5] usageerror --- pytest_django/plugin.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/pytest_django/plugin.py b/pytest_django/plugin.py index e1df02908..a50894397 100644 --- a/pytest_django/plugin.py +++ b/pytest_django/plugin.py @@ -335,6 +335,18 @@ def pytest_configure(config): django_msg = (e.args[0] + "\n\n") if e.args else "" msg = django_msg + _django_project_scan_outcome + # Use UsageError here. + # - the traceback is not interesting, and raising the ImportError would + # make pytest prefix it with INTERNALERROR even. + # - pytest.exit writes to both stdout and stderr?! + # + # The useful information from the header (_dsm_report_header) is not + # there in any case.. + from _pytest.main import UsageError + raise UsageError( + "pytest-django could not setup Django due to ImportError: " + msg + ) + _setup_django()