From 4ca85546a52227b9ef33d5ab328a2f00625a8483 Mon Sep 17 00:00:00 2001 From: Mike Edmunds Date: Sun, 3 May 2026 17:14:45 -0700 Subject: [PATCH 1/2] Refs #35514 -- Fixed settings deprecation warning helper. Replaced the (currently unused) LazySettings._show_deprecation_warning() with a module-level _show_settings_deprecation_warning() function. The new function can be called from any settings-related code, not just LazySettings methods. It correctly distinguishes internal from external settings usage when override_settings() is involved. --- django/conf/__init__.py | 44 +++++++++++++++++++++++++++-------------- 1 file changed, 29 insertions(+), 15 deletions(-) diff --git a/django/conf/__init__.py b/django/conf/__init__.py index dd0a158dfb9a..16f5d9b575dc 100644 --- a/django/conf/__init__.py +++ b/django/conf/__init__.py @@ -147,21 +147,6 @@ def configured(self): """Return True if the settings have already been configured.""" return self._wrapped is not empty - def _show_deprecation_warning(self, message, category): - """Issue a warning when external code uses a deprecated setting. - - Allow Django's own code to use it without emitting the warning. This - function should only be called from within LazySettings methods. - """ - warn_about_external_use( - message, - category, - skip_name_prefixes=( - "django.conf.LazySettings", - "django.utils.functional.LazyObject", - ), - ) - class Settings: def __init__(self, settings_module): @@ -275,4 +260,33 @@ def __repr__(self): } +def _show_settings_deprecation_warning(message, category): + """Issue a warning when external code uses a deprecated setting. + + Allow Django's own code to use the setting without emitting the warning. + This function should only be called from within settings-related code. + """ + warn_about_external_use( + message, + category, + skip_name_prefixes=( + # Include all settings-related code here. (Do not include all of + # "django.conf", which would incorrectly identify any deprecated + # settings usage inside django.conf.urls as external.) + "django.conf.LazySettings", + "django.conf.Settings", + "django.conf.UserSettingsHolder", + "django.utils.functional.LazyObject", # LazySettings superclass. + # override_settings() and similar test utils must be treated as + # settings-related code, else deprecated settings usage in tests + # would be incorrectly identified as internal. + "django.test.utils.override_settings", + "django.test.utils.modify_settings", + "django.test.utils.TestContextDecorator", + "django.test.testcases.SimpleTestCase.settings", + "django.test.testcases.SimpleTestCase.modify_settings", + ), + ) + + settings = LazySettings() From 3e0190a4586e71c92ead3c01323bf0cb9fe1a03c Mon Sep 17 00:00:00 2001 From: Mike Edmunds Date: Mon, 30 Mar 2026 11:48:52 -0700 Subject: [PATCH 2/2] Refs #35514 -- Decoupled settings from functional EmailBackend tests. Reworked tests/mail/test_backends.py so that cases covering functional behavior don't depend on EMAIL_BACKEND or other EMAIL_* settings. (But kept unchanged existing tests to verify backend instance properties are initialized from EMAIL_* settings.) Most backend behavior tests had implicitly relied on email settings overrides in test setup (e.g., to use an emulated SMTP server). They either used mail.get_connection(...) or directly constructed a backend class instance with the specific attributes being tested, relying on the settings overrides to initialize other required attributes. That approach won't work after those settings are deprecated as part of EMAIL_PROVIDERS. Instead, replaced backend construction in "functional" tests with new SharedEmailBackendTests.create_backend() which constructs the testable backend instance with _all_ options needed to avoid global settings. Tests to verify the settings are read correctly continue to directly construct backend instances, without using create_backend(). --- tests/mail/test_backends.py | 153 ++++++++++++++++++------------------ 1 file changed, 77 insertions(+), 76 deletions(-) diff --git a/tests/mail/test_backends.py b/tests/mail/test_backends.py index ba1f652b6d66..9a3c20d99202 100644 --- a/tests/mail/test_backends.py +++ b/tests/mail/test_backends.py @@ -13,10 +13,9 @@ from django.core import mail from django.core.exceptions import ImproperlyConfigured from django.core.mail import EmailMessage -from django.core.mail.backends import dummy, filebased, locmem, smtp +from django.core.mail.backends import console, dummy, filebased, locmem, smtp from django.core.mail.backends.base import BaseEmailBackend from django.test import SimpleTestCase, override_settings -from django.utils.module_loading import import_string from .tests import MailTestsMixin, message_from_bytes @@ -44,12 +43,19 @@ def test_unknown_kwargs_ignored(self): class SharedEmailBackendTests(MailTestsMixin): """Common test cases run against each EmailBackend.""" - email_backend = None + # Subclasses must set to the EmailBackend class being tested. + backend_class = None - @classmethod - def setUpClass(cls): - cls.enterClassContext(override_settings(EMAIL_BACKEND=cls.email_backend)) - super().setUpClass() + # Create an instance of the backend_class for use in this test context + # (configured for use with get_mailbox_content() and flush_mailbox()). + # Subclasses should override to default kwargs for testing if needed. + def create_backend(self, **kwargs): + if self.backend_class is None: + raise NotImplementedError( + "Subclasses of SharedEmailBackendTests must provide a " + "backend_class attribute." + ) + return self.backend_class(**kwargs) def get_mailbox_content(self): raise NotImplementedError( @@ -77,7 +83,7 @@ def test_send(self): email = EmailMessage( "Subject", "Content\n", "from@example.com", ["to@example.com"] ) - num_sent = mail.get_connection().send_messages([email]) + num_sent = self.create_backend().send_messages([email]) self.assertEqual(num_sent, 1) message = self.get_the_message() self.assertEqual(message["subject"], "Subject") @@ -92,7 +98,7 @@ def test_send_unicode(self): "from@example.com", ["to@example.com"], ) - num_sent = mail.get_connection().send_messages([email]) + num_sent = self.create_backend().send_messages([email]) self.assertEqual(num_sent, 1) message = self.get_the_message() self.assertEqual(message["subject"], "Chère maman") @@ -105,7 +111,7 @@ def test_send_many(self): emails_lists = ([email1, email2], iter((email1, email2))) for emails_list in emails_lists: with self.subTest(emails_list=repr(emails_list)): - num_sent = mail.get_connection().send_messages(emails_list) + num_sent = self.create_backend().send_messages(emails_list) self.assertEqual(num_sent, 2) messages = self.get_mailbox_content() self.assertEqual(len(messages), 2) @@ -114,11 +120,11 @@ def test_send_many(self): self.flush_mailbox() def test_connection_can_be_closed_even_if_not_opened(self): - backend = mail.get_connection() + backend = self.create_backend() backend.close() def test_connection_can_be_used_as_contextmanager(self): - backend = mail.get_connection() + backend = self.create_backend() backend.open = mock.Mock() backend.close = mock.Mock() @@ -130,20 +136,18 @@ def test_connection_can_be_used_as_contextmanager(self): backend.close.assert_called_once() def test_fail_silently_arg_accepted(self): - backend_class = import_string(self.email_backend) for value in [True, False]: with self.subTest(fail_silently=value): - backend = backend_class(fail_silently=value) + backend = self.create_backend(fail_silently=value) self.assertIs(backend.fail_silently, value) def test_unknown_kwargs_ignored(self): - backend_class = import_string(self.email_backend) - backend = backend_class(unknown_kwarg="foo") + backend = self.create_backend(unknown_kwarg="foo") self.assertFalse(hasattr(backend, "unknown_kwarg")) class DummyBackendTests(SharedEmailBackendTests, SimpleTestCase): - email_backend = "django.core.mail.backends.dummy.EmailBackend" + backend_class = dummy.EmailBackend def get_mailbox_content(self): # Shared tests that examine the content of sent messages are not @@ -155,13 +159,13 @@ def flush_mailbox(self): pass def test_send_messages_returns_sent_count(self): - backend = dummy.EmailBackend() + backend = self.create_backend() email = EmailMessage(to=["to@example.com"]) self.assertEqual(backend.send_messages([email, email, email]), 3) class LocmemBackendTests(SharedEmailBackendTests, SimpleTestCase): - email_backend = "django.core.mail.backends.locmem.EmailBackend" + backend_class = locmem.EmailBackend def get_mailbox_content(self): return [m.message() for m in mail.outbox] @@ -177,8 +181,8 @@ def test_locmem_shared_messages(self): """ Make sure that the locmem backend populates the outbox. """ - backend1 = locmem.EmailBackend() - backend2 = locmem.EmailBackend() + backend1 = self.create_backend() + backend2 = self.create_backend() email = EmailMessage(to=["to@example.com"]) backend1.send_messages([email]) backend2.send_messages([email]) @@ -188,7 +192,7 @@ def test_validate_multiline_headers(self): # Headers are validated when using the locmem backend (#18861). # (See also EmailMessageTests.test_header_injection().) email = EmailMessage(subject="Subject\nMultiline", to=["to@example.com"]) - backend = locmem.EmailBackend() + backend = self.create_backend() with self.assertRaises(ValueError): backend.send_messages([email]) @@ -197,7 +201,7 @@ def test_outbox_not_mutated_after_send(self): subject="correct subject", to=["to@example.com"], ) - backend = locmem.EmailBackend() + backend = self.create_backend() backend.send_messages([email]) email.subject = "other subject" email.to.append("other@example.com") @@ -206,14 +210,11 @@ def test_outbox_not_mutated_after_send(self): class FileBackendTests(SharedEmailBackendTests, SimpleTestCase): - email_backend = "django.core.mail.backends.filebased.EmailBackend" + backend_class = filebased.EmailBackend def setUp(self): super().setUp() self.tmp_dir = self.mkdtemp() - _settings_override = override_settings(EMAIL_FILE_PATH=self.tmp_dir) - _settings_override.enable() - self.addCleanup(_settings_override.disable) def mkdtemp(self): tmp_dir = tempfile.mkdtemp() @@ -228,6 +229,10 @@ def get_messages_from_filename(self, filename): messages = fp.read().split(b"\n" + (b"-" * 79) + b"\n") return [message_from_bytes(m) for m in messages if m] + def create_backend(self, **kwargs): + kwargs.setdefault("file_path", self.tmp_dir) + return super().create_backend(**kwargs) + def flush_mailbox(self): for filename in self.get_filenames(): os.unlink(os.path.join(self.tmp_dir, filename)) @@ -255,10 +260,7 @@ def test_email_file_path_override_settings(self): def test_error_if_email_file_path_setting_not_defined(self): msg = "The EMAIL_FILE_PATH setting must be set to use the file EmailBackend." - with ( - self.settings(EMAIL_FILE_PATH=None), - self.assertRaisesMessage(ImproperlyConfigured, msg), - ): + with self.assertRaisesMessage(ImproperlyConfigured, msg): filebased.EmailBackend() def test_error_if_file_path_is_not_directory(self): @@ -271,7 +273,7 @@ def test_error_if_file_path_is_not_directory(self): f"Path for saving email messages exists, but is not a directory: {tmp_file}" ) with self.assertRaisesMessage(ImproperlyConfigured, msg): - filebased.EmailBackend(file_path=tmp_file) + self.create_backend(file_path=tmp_file) @skipIf( sys.platform == "win32", @@ -280,7 +282,7 @@ def test_error_if_file_path_is_not_directory(self): def test_error_if_file_path_cannot_be_created(self): msg = "Could not create directory for saving email messages: /dev/null/foo" with self.assertRaisesMessage(ImproperlyConfigured, msg): - filebased.EmailBackend(file_path="/dev/null/foo") + self.create_backend(file_path="/dev/null/foo") @skipIf( sys.platform == "win32", @@ -291,7 +293,7 @@ def test_error_if_file_path_is_not_writeable(self): self.addCleanup(os.chmod, self.tmp_dir, 0o777) msg = f"Could not write to directory: {self.tmp_dir}" with self.assertRaisesMessage(ImproperlyConfigured, msg): - filebased.EmailBackend(file_path=self.tmp_dir) + self.create_backend(file_path=self.tmp_dir) def test_new_file_per_instance(self): # Documented behavior: "A new file is created for each new session that @@ -299,17 +301,17 @@ def test_new_file_per_instance(self): email = EmailMessage(to=["to@example.com"]) self.assertEqual(len(self.get_filenames()), 0) - backend1 = mail.get_connection() + backend1 = self.create_backend() backend1.send_messages([email]) self.assertEqual(len(self.get_filenames()), 1) - backend2 = mail.get_connection() + backend2 = self.create_backend() backend2.send_messages([email]) self.assertEqual(len(self.get_filenames()), 2) def test_multiple_messages_same_connection_single_file_reused(self): self.assertEqual(len(self.get_filenames()), 0) - backend = mail.get_connection() + backend = self.create_backend() self.assertIs(backend.open(), True) backend.send_messages([EmailMessage(to=["one@example.com"])]) @@ -331,7 +333,7 @@ def test_multiple_messages_same_connection_single_file_reused(self): def test_reopening_connection_uses_same_file(self): self.assertEqual(len(self.get_filenames()), 0) - backend = mail.get_connection() + backend = self.create_backend() self.assertIs(backend.open(), True) backend.send_messages([EmailMessage(to=["one@example.com"])]) backend.close() @@ -360,7 +362,7 @@ def mkdtemp(self): class ConsoleBackendTests(SharedEmailBackendTests, SimpleTestCase): - email_backend = "django.core.mail.backends.console.EmailBackend" + backend_class = console.EmailBackend def setUp(self): super().setUp() @@ -382,9 +384,7 @@ def get_mailbox_content(self): def test_console_stream_kwarg(self): s = StringIO() - backend = mail.get_connection( - "django.core.mail.backends.console.EmailBackend", stream=s - ) + backend = self.create_backend(stream=s) backend.send_messages([EmailMessage(to=["to@example.com"])]) message = s.getvalue().split("\n" + ("-" * 79) + "\n")[0].encode() self.assertMessageHasHeaders( @@ -442,12 +442,6 @@ def setUpClass(cls): hostname="127.0.0.1", port=port, ) - cls._settings_override = override_settings( - EMAIL_HOST=cls.smtp_controller.hostname, - EMAIL_PORT=cls.smtp_controller.port, - ) - cls._settings_override.enable() - cls.addClassCleanup(cls._settings_override.disable) cls.smtp_controller.start() cls.addClassCleanup(cls.stop_smtp) @@ -458,13 +452,18 @@ def stop_smtp(cls): @skipUnless(HAS_AIOSMTPD, "No aiosmtpd library detected.") class SMTPBackendTests(SharedEmailBackendTests, SMTPBackendTestsBase): - email_backend = "django.core.mail.backends.smtp.EmailBackend" + backend_class = smtp.EmailBackend def setUp(self): super().setUp() self.smtp_handler.flush_mailbox() self.addCleanup(self.smtp_handler.flush_mailbox) + def create_backend(self, **kwargs): + kwargs.setdefault("host", self.smtp_controller.hostname) + kwargs.setdefault("port", self.smtp_controller.port) + return super().create_backend(**kwargs) + def flush_mailbox(self): self.smtp_handler.flush_mailbox() @@ -493,7 +492,7 @@ def test_email_host_override_settings(self): self.assertEqual(backend.port, 5322) def test_smtp_connection_uses_host_and_port(self): - backend = smtp.EmailBackend(host="mail.example.com", port=5322) + backend = self.create_backend(host="mail.example.com", port=5322) self.assertEqual(backend.host, "mail.example.com") self.assertEqual(backend.port, 5322) with ( @@ -538,7 +537,7 @@ def test_auth_attempted(self): Opening the backend with non empty username/password tries to authenticate against the SMTP server. """ - backend = smtp.EmailBackend( + backend = self.create_backend( username="not empty username", password="not empty password" ) with mock.patch("smtplib.SMTP.login") as mock_smtp_login, backend: @@ -553,14 +552,14 @@ def test_server_open(self): """ open() returns whether it opened a connection. """ - backend = smtp.EmailBackend() + backend = self.create_backend() self.assertIsNone(backend.connection) opened = backend.open() backend.close() self.assertIs(opened, True) def test_reopen_connection(self): - backend = smtp.EmailBackend() + backend = self.create_backend() # Simulate an already open connection. backend.connection = mock.Mock(spec=object()) self.assertIs(backend.open(), False) @@ -576,7 +575,7 @@ def test_email_tls_override_settings(self): self.assertIs(backend.use_tls, False) def test_email_tls_default_disabled(self): - backend = smtp.EmailBackend() + backend = self.create_backend() self.assertIs(backend.use_tls, False) def test_ssl_tls_mutually_exclusive(self): @@ -585,7 +584,7 @@ def test_ssl_tls_mutually_exclusive(self): "one of those settings to True." ) with self.assertRaisesMessage(ValueError, msg): - smtp.EmailBackend(use_ssl=True, use_tls=True) + self.create_backend(use_ssl=True, use_tls=True) @override_settings(EMAIL_USE_SSL=True) def test_email_ssl_use_settings(self): @@ -598,7 +597,7 @@ def test_email_ssl_override_settings(self): self.assertIs(backend.use_ssl, False) def test_email_ssl_default_disabled(self): - backend = smtp.EmailBackend() + backend = self.create_backend() self.assertIs(backend.use_ssl, False) @override_settings(EMAIL_SSL_CERTFILE="foo") @@ -612,7 +611,7 @@ def test_email_ssl_certfile_override_settings(self): self.assertEqual(backend.ssl_certfile, "bar") def test_email_ssl_certfile_default_disabled(self): - backend = smtp.EmailBackend() + backend = self.create_backend() self.assertIsNone(backend.ssl_certfile) @override_settings(EMAIL_SSL_KEYFILE="foo") @@ -626,11 +625,11 @@ def test_email_ssl_keyfile_override_settings(self): self.assertEqual(backend.ssl_keyfile, "bar") def test_email_ssl_keyfile_default_disabled(self): - backend = smtp.EmailBackend() + backend = self.create_backend() self.assertIsNone(backend.ssl_keyfile) def test_ssl_context_uses_ssl_certfile_and_keyfile(self): - backend = smtp.EmailBackend(ssl_certfile="certfile", ssl_keyfile="keyfile") + backend = self.create_backend(ssl_certfile="certfile", ssl_keyfile="keyfile") with mock.patch( "django.core.mail.backends.smtp.ssl.SSLContext" ) as mock_ssl_context: @@ -639,9 +638,8 @@ def test_ssl_context_uses_ssl_certfile_and_keyfile(self): mock_ssl_context.assert_called_once_with(protocol=ssl.PROTOCOL_TLS_CLIENT) ssl_context.load_cert_chain.assert_called_once_with("certfile", "keyfile") - @override_settings(EMAIL_USE_TLS=True) def test_email_tls_attempts_starttls(self): - backend = smtp.EmailBackend() + backend = self.create_backend(use_tls=True) self.assertIs(backend.use_tls, True) with self.assertRaisesMessage( SMTPException, "STARTTLS extension not supported by server." @@ -649,16 +647,15 @@ def test_email_tls_attempts_starttls(self): with backend: pass - @override_settings(EMAIL_USE_SSL=True) def test_email_ssl_attempts_ssl_connection(self): - backend = smtp.EmailBackend() + backend = self.create_backend(use_ssl=True) self.assertIs(backend.use_ssl, True) with self.assertRaises(SSLError): with backend: pass def test_connection_timeout_default(self): - backend = mail.get_connection("django.core.mail.backends.smtp.EmailBackend") + backend = self.create_backend() self.assertIsNone(backend.timeout) def test_connection_timeout_custom(self): @@ -669,7 +666,9 @@ def __init__(self, *args, **kwargs): kwargs.setdefault("timeout", 42) super().__init__(*args, **kwargs) - myemailbackend = MyEmailBackend() + myemailbackend = MyEmailBackend( + host=self.smtp_controller.hostname, port=self.smtp_controller.port + ) myemailbackend.open() self.assertEqual(myemailbackend.timeout, 42) self.assertEqual(myemailbackend.connection.timeout, 42) @@ -686,12 +685,12 @@ def test_email_timeout_override_settings(self): self.assertEqual(backend.timeout, 15) def test_smtp_connection_uses_timeout(self): - backend = smtp.EmailBackend(timeout=10) + backend = self.create_backend(timeout=10) with backend: self.assertEqual(backend.connection.timeout, 10) def test_serialized_message_uses_crlf_line_ending(self): - backend = mail.get_connection() + backend = self.create_backend() with ( backend, mock.patch.object(backend.connection, "sendmail") as mock_sendmail, @@ -712,7 +711,7 @@ def test_send_messages_after_open_failed(self): send_messages() shouldn't try to send messages if open() raises an exception after initializing the connection. """ - backend = smtp.EmailBackend() + backend = self.create_backend() # Simulate connection initialization success and a subsequent # connection exception. backend.connection = mock.Mock() @@ -721,14 +720,14 @@ def test_send_messages_after_open_failed(self): self.assertEqual(backend.send_messages([email]), 0) def test_send_messages_with_empty_list_does_not_open_connection(self): - backend = smtp.EmailBackend() + backend = self.create_backend() backend.open = mock.Mock() self.assertEqual(backend.send_messages([]), 0) backend.open.assert_not_called() def test_send_messages_zero_sent(self): """A message isn't sent if it doesn't have any recipients.""" - backend = smtp.EmailBackend() + backend = self.create_backend() backend.connection = mock.Mock() email = EmailMessage("Subject", "Content", "from@example.com", to=[]) sent = backend.send_messages([email]) @@ -741,7 +740,7 @@ def test_avoids_sending_to_invalid_addresses(self): EmailMessage.all_recipients() (which is distinct from message header fields). """ - backend = smtp.EmailBackend() + backend = self.create_backend() backend.connection = mock.Mock() for email_address in ( # Invalid address with two @ signs. @@ -780,7 +779,7 @@ def test_encodes_idna_in_smtp_commands(self): "To": "Discussão Django ", }, ) - backend = smtp.EmailBackend() + backend = self.create_backend() backend.send_messages([email]) envelope = self.get_smtp_envelopes()[0] self.assertEqual(envelope["mail_from"], "lists@xn--discusso-xza.example.org") @@ -805,7 +804,7 @@ def test_does_not_reencode_idna(self): cc=['"ශ්‍රී" '], bcc=['"نامه‌ای." '], ) - backend = smtp.EmailBackend() + backend = self.create_backend() backend.send_messages([email]) envelope = self.get_smtp_envelopes()[0] self.assertEqual(envelope["mail_from"], "from@xn--fa-hia.example.com") @@ -823,7 +822,7 @@ def test_rejects_non_ascii_local_part(self): The SMTP EmailBackend does not currently support non-ASCII local-parts. (That would require using the RFC 6532 SMTPUTF8 extension.) #35713. """ - backend = smtp.EmailBackend() + backend = self.create_backend() backend.connection = mock.Mock(spec=object()) email = EmailMessage(to=["nø@example.dk"]) with self.assertRaisesMessage( @@ -835,7 +834,7 @@ def test_rejects_non_ascii_local_part(self): def test_prep_address_without_force_ascii(self): # A subclass implementing SMTPUTF8 could use # prep_address(force_ascii=False). - backend = smtp.EmailBackend() + backend = self.create_backend() for case in ["åh@example.dk", "oh@åh.example.dk", "åh@åh.example.dk"]: with self.subTest(case=case): self.assertEqual(backend.prep_address(case, force_ascii=False), case) @@ -846,7 +845,9 @@ class SMTPBackendStoppedServerTests(SMTPBackendTestsBase): @classmethod def setUpClass(cls): super().setUpClass() - cls.backend = smtp.EmailBackend() + cls.backend = smtp.EmailBackend( + host=cls.smtp_controller.hostname, port=cls.smtp_controller.port + ) cls.smtp_controller.stop() @classmethod