Skip to content

[WIP/RFC] Setup Django only later (pytest_configure) #719

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
123 changes: 63 additions & 60 deletions pytest_django/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
test database and provides some useful text fixtures.
"""

import contextlib
import inspect
from functools import reduce
import os
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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()


Expand Down
4 changes: 2 additions & 2 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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 {}))
Expand Down
3 changes: 1 addition & 2 deletions tests/test_database.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
]
)

Expand Down
80 changes: 59 additions & 21 deletions tests/test_initialization.py
Original file line number Diff line number Diff line change
@@ -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
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's probably a dealbreaker: not being able to import models in your conftest at the toplevel anymore?!


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(
Expand All @@ -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
"""
)
Expand All @@ -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