Skip to content

Commit 0a8473e

Browse files
authored
Fix possible AttributeError: module 'django.core.mail' has no attribute 'outbox' errors (#1187)
This error happened when the `locmem` email backend is not configured and the `_dj_autoclear_mailbox` autouse fixture kicked in. Fix #993
1 parent 5ada9c1 commit 0a8473e

File tree

3 files changed

+73
-6
lines changed

3 files changed

+73
-6
lines changed

pytest_django/plugin.py

+6-4
Original file line numberDiff line numberDiff line change
@@ -599,7 +599,8 @@ def _dj_autoclear_mailbox() -> None:
599599

600600
from django.core import mail
601601

602-
del mail.outbox[:]
602+
if hasattr(mail, "outbox"):
603+
mail.outbox.clear()
603604

604605

605606
@pytest.fixture()
@@ -608,12 +609,13 @@ def mailoutbox(
608609
_dj_autoclear_mailbox: None,
609610
) -> list[django.core.mail.EmailMessage] | None:
610611
"""A clean email outbox to which Django-generated emails are sent."""
611-
if not django_settings_is_configured():
612-
return None
612+
skip_if_no_django()
613613

614614
from django.core import mail
615615

616-
return mail.outbox # type: ignore[no-any-return]
616+
if hasattr(mail, "outbox"):
617+
return mail.outbox # type: ignore[no-any-return]
618+
return []
617619

618620

619621
@pytest.fixture()

tests/conftest.py

+8-2
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,13 @@ def _marker_apifun(
3030
extra_settings: str = "",
3131
create_manage_py: bool = False,
3232
project_root: str | None = None,
33+
create_settings: bool = True,
3334
):
3435
return {
3536
"extra_settings": extra_settings,
3637
"create_manage_py": create_manage_py,
3738
"project_root": project_root,
39+
"create_settings": create_settings,
3840
}
3941

4042

@@ -135,14 +137,18 @@ def django_pytester(
135137

136138
# Copy the test app to make it available in the new test run
137139
shutil.copytree(str(app_source), str(test_app_path))
138-
tpkg_path.joinpath("the_settings.py").write_text(test_settings)
140+
if options["create_settings"]:
141+
tpkg_path.joinpath("the_settings.py").write_text(test_settings)
139142

140143
# For suprocess tests, pytest's `pythonpath` setting doesn't currently
141144
# work, only the envvar does.
142145
pythonpath = os.pathsep.join(filter(None, [str(REPOSITORY_ROOT), os.getenv("PYTHONPATH", "")]))
143146
monkeypatch.setenv("PYTHONPATH", pythonpath)
144147

145-
monkeypatch.setenv("DJANGO_SETTINGS_MODULE", "tpkg.the_settings")
148+
if options["create_settings"]:
149+
monkeypatch.setenv("DJANGO_SETTINGS_MODULE", "tpkg.the_settings")
150+
else:
151+
monkeypatch.delenv("DJANGO_SETTINGS_MODULE", raising=False)
146152

147153
def create_test_module(test_code: str, filename: str = "test_the_test.py") -> Path:
148154
r = tpkg_path.joinpath(filename)

tests/test_fixtures.py

+59
Original file line numberDiff line numberDiff line change
@@ -825,3 +825,62 @@ def mocked_make_msgid(*args, **kwargs):
825825
result = django_pytester.runpytest_subprocess("--tb=short", "-vv", "-s")
826826
result.stdout.fnmatch_lines(["*test_mailbox_inner*", "django_mail_dnsname_mark", "PASSED*"])
827827
assert result.ret == 0
828+
829+
830+
@pytest.mark.django_project(
831+
create_manage_py=True,
832+
extra_settings="""
833+
EMAIL_BACKEND = "django.core.mail.backends.dummy.EmailBackend"
834+
""",
835+
)
836+
def test_mail_auto_fixture_misconfigured(django_pytester: DjangoPytester) -> None:
837+
"""
838+
django_test_environment fixture can be overridden by user, and that would break mailoutbox fixture.
839+
840+
Normally settings.EMAIL_BACKEND is set to "django.core.mail.backends.locmem.EmailBackend" by django,
841+
along with mail.outbox = []. If this function doesn't run for whatever reason, the
842+
mailoutbox fixture will not work properly.
843+
"""
844+
django_pytester.create_test_module(
845+
"""
846+
import pytest
847+
848+
@pytest.fixture(autouse=True, scope="session")
849+
def django_test_environment(request):
850+
yield
851+
""",
852+
filename="conftest.py",
853+
)
854+
855+
django_pytester.create_test_module(
856+
"""
857+
def test_with_fixture(settings, mailoutbox):
858+
assert mailoutbox == []
859+
assert settings.EMAIL_BACKEND == "django.core.mail.backends.dummy.EmailBackend"
860+
861+
def test_without_fixture():
862+
from django.core import mail
863+
assert not hasattr(mail, "outbox")
864+
"""
865+
)
866+
result = django_pytester.runpytest_subprocess()
867+
result.assert_outcomes(passed=2)
868+
869+
870+
@pytest.mark.django_project(create_settings=False)
871+
def test_no_settings(django_pytester: DjangoPytester) -> None:
872+
django_pytester.create_test_module(
873+
"""
874+
def test_skipped_settings(settings):
875+
assert False
876+
877+
def test_skipped_mailoutbox(mailoutbox):
878+
assert False
879+
880+
def test_mail():
881+
from django.core import mail
882+
assert not hasattr(mail, "outbox")
883+
"""
884+
)
885+
result = django_pytester.runpytest_subprocess()
886+
result.assert_outcomes(passed=1, skipped=2)

0 commit comments

Comments
 (0)