diff --git a/pytest_django/plugin.py b/pytest_django/plugin.py index 09c61c801..a50894397 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,101 @@ 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.trylast +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)" % ( - ds, - ds_source, - ) - else: - early_config._dsm_report_header = None + if not ds: + config._dsm_report_header = None - # Configure DJANGO_CONFIGURATION - dc = ( - options.dc - or os.environ.get(CONFIGURATION_ENV) - or early_config.getini(CONFIGURATION_ENV) - ) - - if ds: - os.environ[SETTINGS_MODULE_ENV] = ds - - if dc: - os.environ[CONFIGURATION_ENV] = dc + # Setup Django if settings are configured manually. + _setup_django() - # Install the django-configurations importer - import configurations.importer + return - configurations.importer.install() + config._dsm_report_header = "Django settings: %s (from %s)" % ( + ds, + ds_source, + ) - # Forcefully load Django settings, throws ImportError or - # ImproperlyConfigured if settings cannot be loaded. - from django.conf import settings as dj_settings + os.environ[SETTINGS_MODULE_ENV] = ds - with _handle_import_error(_django_project_scan_outcome): - dj_settings.DATABASES + # Configure DJANGO_CONFIGURATION + dc = ( + config.option.dc + or os.environ.get(CONFIGURATION_ENV) + or config.getini(CONFIGURATION_ENV) + ) + if dc: + os.environ[CONFIGURATION_ENV] = dc - _setup_django() + # Install the django-configurations importer + import configurations.importer + configurations.importer.install() -def pytest_report_header(config): - if config._dsm_report_header: - return [config._dsm_report_header] + # Forcefully load Django settings. + # Throws ImportError or ImproperlyConfigured if settings cannot be loaded. + from django.conf import settings as dj_settings + 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 + + # 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 + ) -@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() 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 {})) 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.", ] ) 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