diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py index 31d9d567bc56..fd4c644b048b 100644 --- a/django/contrib/admin/options.py +++ b/django/contrib/admin/options.py @@ -63,7 +63,7 @@ from django.template.response import SimpleTemplateResponse, TemplateResponse from django.urls import reverse from django.utils.decorators import method_decorator -from django.utils.deprecation import RemovedInDjango70Warning +from django.utils.deprecation import RemovedInDjango70Warning, warn_about_implementation from django.utils.html import format_html from django.utils.http import urlencode from django.utils.inspect import get_func_args @@ -928,11 +928,12 @@ def get_changelist_instance(self, request): # RemovedInDjango70Warning: when the deprecation ends, remove the # below 'if' clause and raise a ValueError here. if self.list_select_related is not True: - warnings.warn( + warn_about_implementation( "Returning True from ModelAdmin.get_list_select_related() is " "deprecated. Return False or a list or tuple of fields to " "fetch instead.", RemovedInDjango70Warning, + self.get_list_select_related, ) return ChangeList( request, @@ -1132,12 +1133,12 @@ def _get_actions_with_action_location( if "action_location" in get_func_args(self.get_actions): return self.get_actions(request, action_location=action_location) else: - warnings.warn( + warn_about_implementation( "Overriding get_actions() without the 'action_location' parameter is " "deprecated. Update the signature to get_actions(self, request, " "action_location=ActionLocation.CHANGE_LIST).", RemovedInDjango70Warning, - skip_file_prefixes=django_file_prefixes(), + self.get_actions, ) if action_location == ActionLocation.CHANGE_FORM: # Disable adding actions on change form when get_actions is @@ -1172,13 +1173,13 @@ def _get_action_choices_with_action_location( action_location=action_location, ) else: - warnings.warn( + warn_about_implementation( "Overriding get_action_choices() without the 'action_location' " "parameter is deprecated. Update the signature to " "get_action_choices(self, request, default_choices=None, " "action_location=ActionLocation.CHANGE_LIST).", RemovedInDjango70Warning, - skip_file_prefixes=django_file_prefixes(), + self.get_action_choices, ) return self.get_action_choices(request, default_choices=default_choices) diff --git a/django/contrib/admindocs/middleware.py b/django/contrib/admindocs/middleware.py index 5c9f08d0cc93..48a3cb3e7422 100644 --- a/django/contrib/admindocs/middleware.py +++ b/django/contrib/admindocs/middleware.py @@ -1,7 +1,7 @@ from django.conf import settings from django.core.exceptions import ImproperlyConfigured from django.http import HttpResponse -from django.utils.deprecation import MiddlewareMixin +from django.middleware import MiddlewareMixin from .utils import get_view_name diff --git a/django/contrib/auth/middleware.py b/django/contrib/auth/middleware.py index 09e3d3e6bbc6..f3735c501daa 100644 --- a/django/contrib/auth/middleware.py +++ b/django/contrib/auth/middleware.py @@ -9,8 +9,8 @@ from django.contrib.auth.views import redirect_to_login from django.core.exceptions import ImproperlyConfigured from django.core.handlers.asgi import ASGIRequest +from django.middleware import MiddlewareMixin from django.shortcuts import resolve_url -from django.utils.deprecation import MiddlewareMixin from django.utils.functional import SimpleLazyObject diff --git a/django/contrib/flatpages/middleware.py b/django/contrib/flatpages/middleware.py index 9c6a7273ccce..00017ad4daea 100644 --- a/django/contrib/flatpages/middleware.py +++ b/django/contrib/flatpages/middleware.py @@ -1,7 +1,7 @@ from django.conf import settings from django.contrib.flatpages.views import flatpage from django.http import Http404 -from django.utils.deprecation import MiddlewareMixin +from django.middleware import MiddlewareMixin class FlatpageFallbackMiddleware(MiddlewareMixin): diff --git a/django/contrib/messages/middleware.py b/django/contrib/messages/middleware.py index 00870318d1b7..c3bf59d0b2ed 100644 --- a/django/contrib/messages/middleware.py +++ b/django/contrib/messages/middleware.py @@ -1,6 +1,6 @@ from django.conf import settings from django.contrib.messages.storage import default_storage -from django.utils.deprecation import MiddlewareMixin +from django.middleware import MiddlewareMixin class MessageMiddleware(MiddlewareMixin): diff --git a/django/contrib/redirects/middleware.py b/django/contrib/redirects/middleware.py index bc4fe9cbcd78..3c4beb7e889e 100644 --- a/django/contrib/redirects/middleware.py +++ b/django/contrib/redirects/middleware.py @@ -4,7 +4,7 @@ from django.contrib.sites.shortcuts import get_current_site from django.core.exceptions import ImproperlyConfigured from django.http import HttpResponseGone, HttpResponsePermanentRedirect -from django.utils.deprecation import MiddlewareMixin +from django.middleware import MiddlewareMixin class RedirectFallbackMiddleware(MiddlewareMixin): diff --git a/django/contrib/sessions/middleware.py b/django/contrib/sessions/middleware.py index c6a5e336788c..06217b3dacbc 100644 --- a/django/contrib/sessions/middleware.py +++ b/django/contrib/sessions/middleware.py @@ -4,8 +4,8 @@ from django.conf import settings from django.contrib.sessions.backends.base import UpdateError from django.contrib.sessions.exceptions import SessionInterrupted +from django.middleware import MiddlewareMixin from django.utils.cache import patch_vary_headers -from django.utils.deprecation import MiddlewareMixin from django.utils.http import http_date diff --git a/django/contrib/sites/middleware.py b/django/contrib/sites/middleware.py index bc3bf20c48b5..6333283f3edd 100644 --- a/django/contrib/sites/middleware.py +++ b/django/contrib/sites/middleware.py @@ -1,4 +1,4 @@ -from django.utils.deprecation import MiddlewareMixin +from django.middleware import MiddlewareMixin from .shortcuts import get_current_site diff --git a/django/middleware/__init__.py b/django/middleware/__init__.py index e69de29bb2d1..722b5e9682c8 100644 --- a/django/middleware/__init__.py +++ b/django/middleware/__init__.py @@ -0,0 +1,64 @@ +from inspect import iscoroutinefunction, markcoroutinefunction + +from asgiref.sync import sync_to_async + +__all__ = ["MiddlewareMixin"] + + +class MiddlewareMixin: + sync_capable = True + async_capable = True + + def __init__(self, get_response): + if get_response is None: + raise ValueError("get_response must be provided.") + self.get_response = get_response + # If get_response is a coroutine function, turns us into async mode so + # a thread is not consumed during a whole request. + self.async_mode = iscoroutinefunction(self.get_response) + if self.async_mode: + # Mark the class as async-capable, but do the actual switch inside + # __call__ to avoid swapping out dunder methods. + markcoroutinefunction(self) + super().__init__() + + def __repr__(self): + return "<%s get_response=%s>" % ( + self.__class__.__qualname__, + getattr( + self.get_response, + "__qualname__", + self.get_response.__class__.__name__, + ), + ) + + def __call__(self, request): + # Exit out to async mode, if needed + if self.async_mode: + return self.__acall__(request) + response = None + if hasattr(self, "process_request"): + response = self.process_request(request) + response = response or self.get_response(request) + if hasattr(self, "process_response"): + response = self.process_response(request, response) + return response + + async def __acall__(self, request): + """ + Async version of __call__ that is swapped in when an async request + is running. + """ + response = None + if hasattr(self, "process_request"): + response = await sync_to_async( + self.process_request, + thread_sensitive=True, + )(request) + response = response or await self.get_response(request) + if hasattr(self, "process_response"): + response = await sync_to_async( + self.process_response, + thread_sensitive=True, + )(request, response) + return response diff --git a/django/middleware/cache.py b/django/middleware/cache.py index 21bdad33f408..ea6decb83c23 100644 --- a/django/middleware/cache.py +++ b/django/middleware/cache.py @@ -49,6 +49,7 @@ from django.conf import settings from django.core.cache import DEFAULT_CACHE_ALIAS, caches +from django.middleware import MiddlewareMixin from django.utils.cache import ( get_cache_key, get_max_age, @@ -57,7 +58,6 @@ patch_response_headers, patch_vary_headers, ) -from django.utils.deprecation import MiddlewareMixin from django.utils.http import parse_http_date_safe, split_directive_names diff --git a/django/middleware/clickjacking.py b/django/middleware/clickjacking.py index f0c99710e639..1c6117963baa 100644 --- a/django/middleware/clickjacking.py +++ b/django/middleware/clickjacking.py @@ -6,7 +6,7 @@ """ from django.conf import settings -from django.utils.deprecation import MiddlewareMixin +from django.middleware import MiddlewareMixin class XFrameOptionsMiddleware(MiddlewareMixin): diff --git a/django/middleware/common.py b/django/middleware/common.py index 8ce17b5c096c..2046675e87f0 100644 --- a/django/middleware/common.py +++ b/django/middleware/common.py @@ -5,8 +5,8 @@ from django.core.exceptions import PermissionDenied from django.core.mail import MailerDoesNotExist, mail_managers, mailers from django.http import HttpResponsePermanentRedirect +from django.middleware import MiddlewareMixin from django.urls import is_valid_path -from django.utils.deprecation import MiddlewareMixin from django.utils.http import escape_leading_slashes diff --git a/django/middleware/csp.py b/django/middleware/csp.py index ba08cfff0c1a..58e2b778f6cb 100644 --- a/django/middleware/csp.py +++ b/django/middleware/csp.py @@ -1,6 +1,6 @@ from django.conf import settings +from django.middleware import MiddlewareMixin from django.utils.csp import CSP, LazyNonce, build_policy -from django.utils.deprecation import MiddlewareMixin def get_nonce(request): diff --git a/django/middleware/csrf.py b/django/middleware/csrf.py index 3c709f2f4b27..d70c3efe3fc3 100644 --- a/django/middleware/csrf.py +++ b/django/middleware/csrf.py @@ -13,10 +13,10 @@ from django.conf import settings from django.core.exceptions import DisallowedHost, ImproperlyConfigured from django.http import HttpHeaders, UnreadablePostError +from django.middleware import MiddlewareMixin from django.urls import get_callable from django.utils.cache import patch_vary_headers from django.utils.crypto import constant_time_compare, get_random_string -from django.utils.deprecation import MiddlewareMixin from django.utils.functional import cached_property from django.utils.http import is_same_domain from django.utils.log import log_response diff --git a/django/middleware/gzip.py b/django/middleware/gzip.py index eb151d7ad5b4..51ae29bcdb0c 100644 --- a/django/middleware/gzip.py +++ b/django/middleware/gzip.py @@ -1,5 +1,5 @@ +from django.middleware import MiddlewareMixin from django.utils.cache import patch_vary_headers -from django.utils.deprecation import MiddlewareMixin from django.utils.regex_helper import _lazy_re_compile from django.utils.text import acompress_sequence, compress_sequence, compress_string diff --git a/django/middleware/http.py b/django/middleware/http.py index b5d2b8a08200..e44d59a9604a 100644 --- a/django/middleware/http.py +++ b/django/middleware/http.py @@ -1,5 +1,5 @@ +from django.middleware import MiddlewareMixin from django.utils.cache import get_conditional_response, set_response_etag -from django.utils.deprecation import MiddlewareMixin from django.utils.http import parse_http_date_safe, split_directive_names diff --git a/django/middleware/locale.py b/django/middleware/locale.py index 71db230da25e..baff2e343416 100644 --- a/django/middleware/locale.py +++ b/django/middleware/locale.py @@ -1,10 +1,10 @@ from django.conf import settings from django.conf.urls.i18n import is_language_prefix_patterns_used from django.http import HttpResponseRedirect +from django.middleware import MiddlewareMixin from django.urls import get_script_prefix, is_valid_path from django.utils import translation from django.utils.cache import patch_vary_headers -from django.utils.deprecation import MiddlewareMixin class LocaleMiddleware(MiddlewareMixin): diff --git a/django/middleware/security.py b/django/middleware/security.py index 52c81e340636..ee182f54493f 100644 --- a/django/middleware/security.py +++ b/django/middleware/security.py @@ -2,7 +2,7 @@ from django.conf import settings from django.http import HttpResponsePermanentRedirect -from django.utils.deprecation import MiddlewareMixin +from django.middleware import MiddlewareMixin class SecurityMiddleware(MiddlewareMixin): diff --git a/django/utils/deprecation.py b/django/utils/deprecation.py index 9b15b4e2e76a..951967e99f8d 100644 --- a/django/utils/deprecation.py +++ b/django/utils/deprecation.py @@ -1,11 +1,11 @@ import functools import inspect +import sys import warnings from collections import Counter -from inspect import iscoroutinefunction, markcoroutinefunction - -from asgiref.sync import sync_to_async +from inspect import iscoroutinefunction +from django.middleware import MiddlewareMixin as _MiddlewareMixin from django.utils.inspect import signature from django.utils.warnings import django_file_prefixes @@ -22,6 +22,18 @@ class RemovedInDjango71Warning(PendingDeprecationWarning): RemovedAfterNextVersionWarning = RemovedInDjango71Warning +def __getattr__(name): + if name == "MiddlewareMixin": + warnings.warn( + "Importing MiddlewareMixin from django.utils.deprecation is deprecated. " + "Import from django.middleware.MiddlewareMixin instead.", + RemovedInDjango71Warning, + stacklevel=2, + ) + return _MiddlewareMixin + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") + + def warn_about_external_use( message, category, @@ -101,6 +113,58 @@ def back(frame, level): warnings.warn(message, category=category, stacklevel=level + 1) +def warn_about_implementation(message, category, target): + """Issue a warning about a specific function, class, or method definition. + + The warning will point to the source filename and line number where + 'target' is _defined_ (not where it is being _called_ as with other warning + helpers). Use this to warn about code that isn't currently on the call + stack, e.g., deprecation based on the return value of a called method or + inspecting a function's signature to see if it supports an updated API. + + Supported usage: + warn_about_implementation(message, category, some_function) + warn_about_implementation(message, category, SomeClass) + warn_about_implementation(message, category, SomeClass.method) + warn_about_implementation(message, category, self.method) [a bound method] + + To warn about a property without invoking its descriptor, call with + 'target' set to inspect.getattr_static(class_or_instance, "property_name"). + """ + if inspect.ismethod(target) or isinstance(target, (classmethod, staticmethod)): + function = target.__func__ + elif inspect.isfunction(target) or inspect.isclass(target): + function = target + elif isinstance(target, property): + function = target.fget + else: + raise TypeError( + "target must be a function, class, bound method, or unbound descriptor " + "(classmethod, staticmethod, or property)." + ) + + function = inspect.unwrap(function) + + try: + filename = inspect.getsourcefile(function) + _, lineno = inspect.getsourcelines(function) + except (AttributeError, OSError, TypeError): + # Can't identify the source. Issue the warning generically. + warnings.warn(message, category, skip_file_prefixes=django_file_prefixes()) + return + + # Find or create the module's warning registry so warning filters work. + module = function.__module__ + try: + registry = sys.modules[module].__dict__.setdefault("__warningregistry__", {}) + except (AttributeError, KeyError): + registry = None + + warnings.warn_explicit( + message, category, filename, lineno, module=module, registry=registry + ) + + class warn_about_renamed_method: def __init__( self, class_name, old_method_name, new_method_name, deprecation_warning @@ -341,62 +405,3 @@ def wrapper(*args, **kwargs): return wrapper return decorator - - -class MiddlewareMixin: - sync_capable = True - async_capable = True - - def __init__(self, get_response): - if get_response is None: - raise ValueError("get_response must be provided.") - self.get_response = get_response - # If get_response is a coroutine function, turns us into async mode so - # a thread is not consumed during a whole request. - self.async_mode = iscoroutinefunction(self.get_response) - if self.async_mode: - # Mark the class as async-capable, but do the actual switch inside - # __call__ to avoid swapping out dunder methods. - markcoroutinefunction(self) - super().__init__() - - def __repr__(self): - return "<%s get_response=%s>" % ( - self.__class__.__qualname__, - getattr( - self.get_response, - "__qualname__", - self.get_response.__class__.__name__, - ), - ) - - def __call__(self, request): - # Exit out to async mode, if needed - if self.async_mode: - return self.__acall__(request) - response = None - if hasattr(self, "process_request"): - response = self.process_request(request) - response = response or self.get_response(request) - if hasattr(self, "process_response"): - response = self.process_response(request, response) - return response - - async def __acall__(self, request): - """ - Async version of __call__ that is swapped in when an async request - is running. - """ - response = None - if hasattr(self, "process_request"): - response = await sync_to_async( - self.process_request, - thread_sensitive=True, - )(request) - response = response or await self.get_response(request) - if hasattr(self, "process_response"): - response = await sync_to_async( - self.process_response, - thread_sensitive=True, - )(request, response) - return response diff --git a/docs/internals/deprecation.txt b/docs/internals/deprecation.txt index 2a4e8aca27a1..e86a456fd943 100644 --- a/docs/internals/deprecation.txt +++ b/docs/internals/deprecation.txt @@ -15,6 +15,9 @@ about each item can often be found in the release notes of two versions prior. See the :ref:`Django 6.2 release notes ` for more details on these changes. +* The ``django.utils.deprecation.MiddlewareMixin`` import path will be removed. + Use ``django.middleware.MiddlewareMixin`` instead. + * The ``safe`` parameter of :class:`~django.http.JsonResponse` will be removed. * Calling ``QuerySet.aiterator()`` after ``prefetch_related()`` without diff --git a/docs/releases/3.1.1.txt b/docs/releases/3.1.1.txt index 32050c31b830..74a445d7fffa 100644 --- a/docs/releases/3.1.1.txt +++ b/docs/releases/3.1.1.txt @@ -55,7 +55,7 @@ Bugfixes ``TemplateView.get_context_data()`` (:ticket:`31877`). * Enforced thread sensitivity of the :class:`MiddlewareMixin.process_request() - ` and ``process_response()`` hooks + ` and ``process_response()`` hooks when in an async context (:ticket:`31905`). * Fixed ``__in`` lookup on key transforms for diff --git a/docs/releases/6.2.txt b/docs/releases/6.2.txt index 2e59060b1563..5e1c37c1effe 100644 --- a/docs/releases/6.2.txt +++ b/docs/releases/6.2.txt @@ -288,6 +288,10 @@ Features deprecated in 6.2 Miscellaneous ------------- +* The :class:`~django.middleware.MiddlewareMixin` class moved from + ``django.utils.deprecation`` to ``django.middleware``. The old import path + is deprecated. + * The ``safe`` parameter is deprecated from :class:`~django.http.JsonResponse`. Omitting the argument is equivalent to the prior ``safe=False`` usage. diff --git a/docs/topics/http/middleware.txt b/docs/topics/http/middleware.txt index 8bca89f56de6..f7c57106e970 100644 --- a/docs/topics/http/middleware.txt +++ b/docs/topics/http/middleware.txt @@ -395,10 +395,10 @@ instances are correctly marked as coroutine functions:: Upgrading pre-Django 1.10-style middleware ========================================== -.. class:: django.utils.deprecation.MiddlewareMixin +.. class:: django.middleware.MiddlewareMixin :module: -Django provides ``django.utils.deprecation.MiddlewareMixin`` to ease creating +Django provides ``django.middleware.MiddlewareMixin`` to ease creating middleware classes that are compatible with both :setting:`MIDDLEWARE` and the old ``MIDDLEWARE_CLASSES``, and support synchronous and asynchronous requests. All middleware classes included with Django are compatible with both settings. @@ -453,3 +453,8 @@ These are the behavioral differences between using :setting:`MIDDLEWARE` and HTTP response, and then the next middleware in line will see that response. Middleware are never skipped due to a middleware raising an exception. + +.. versionchanged:: 6.2 + + ``MiddlewareMixin`` was moved from ``django.utils.deprecation`` to + ``django.middleware``. The old import path is deprecated. diff --git a/tests/admin_views/test_actions.py b/tests/admin_views/test_actions.py index dbed3a33b6f0..f2fc4e3f3587 100644 --- a/tests/admin_views/test_actions.py +++ b/tests/admin_views/test_actions.py @@ -15,6 +15,7 @@ from django.utils.deprecation import RemovedInDjango70Warning from .admin import SubscriberAdmin +from .admin import __file__ as admin_filename from .forms import MediaActionForm from .models import ( Actor, @@ -878,14 +879,14 @@ def test_overridden_admin_action_methods(self): ) with self.assertWarnsMessage( RemovedInDjango70Warning, get_actions_overridden_msg - ): + ) as warning: response = self.client.get(change_url) + self.assertEqual(warning.filename, admin_filename) self.assertNotContains(response, "
", html=True) changelist_url = reverse("admin:admin_views_modelaction_changelist") with warnings.catch_warnings(record=True) as warning_list: warnings.simplefilter("always", RemovedInDjango70Warning) - warnings.filterwarnings("always", category=RemovedInDjango70Warning) response = self.client.get(changelist_url) message_warnings = [str(warning.message) for warning in warning_list] @@ -901,6 +902,9 @@ def test_overridden_admin_action_methods(self): "Use Action attributes instead.", } self.assertEqual(set(message_warnings), expected_warnings) + self.assertEqual( + {warning.filename for warning in warning_list}, {admin_filename} + ) self.assertContains(response, "Delete selected model actions (extra)") action_data = { @@ -915,10 +919,11 @@ def test_overridden_admin_action_methods(self): ) with self.assertWarnsMessage( RemovedInDjango70Warning, get_actions_overridden_msg - ): + ) as warning: response = self.client.post( reverse("admin:admin_views_modelaction_changelist"), action_data ) + self.assertEqual(warning.filename, admin_filename) self.assertContains( response, "Are you sure you want to delete the selected model action?" ) diff --git a/tests/csrf_tests/views.py b/tests/csrf_tests/views.py index 3949aeaea1aa..b681218fc906 100644 --- a/tests/csrf_tests/views.py +++ b/tests/csrf_tests/views.py @@ -1,9 +1,9 @@ from django.http import HttpResponse +from django.middleware import MiddlewareMixin from django.middleware.csrf import get_token, rotate_token from django.template import Context, RequestContext, Template from django.template.context_processors import csrf from django.utils.decorators import decorator_from_middleware -from django.utils.deprecation import MiddlewareMixin from django.views.decorators.csrf import csrf_protect, ensure_csrf_cookie diff --git a/tests/deprecation/test_middleware_mixin.py b/tests/deprecation/test_middleware_mixin.py index 00bc0391edfb..c18b58e08bad 100644 --- a/tests/deprecation/test_middleware_mixin.py +++ b/tests/deprecation/test_middleware_mixin.py @@ -16,6 +16,7 @@ from django.db import connection from django.http.request import HttpRequest from django.http.response import HttpResponse +from django.middleware import MiddlewareMixin from django.middleware.cache import ( CacheMiddleware, FetchFromCacheMiddleware, @@ -29,7 +30,7 @@ from django.middleware.locale import LocaleMiddleware from django.middleware.security import SecurityMiddleware from django.test import SimpleTestCase -from django.utils.deprecation import MiddlewareMixin +from django.utils.deprecation import RemovedInDjango71Warning class MiddlewareMixinTests(SimpleTestCase): @@ -55,6 +56,18 @@ class MiddlewareMixinTests(SimpleTestCase): XViewMiddleware, ] + def test_deprecation_import_compatibility(self): + msg = ( + "Importing MiddlewareMixin from django.utils.deprecation is deprecated. " + "Import from django.middleware.MiddlewareMixin instead." + ) + with self.assertWarnsMessage(RemovedInDjango71Warning, msg): + from django.utils.deprecation import ( + MiddlewareMixin as DeprecatedMiddlewareMixin, + ) + + self.assertIs(DeprecatedMiddlewareMixin, MiddlewareMixin) + def test_repr(self): class GetResponse: def __call__(self): diff --git a/tests/deprecation/test_warn_about_implementation.py b/tests/deprecation/test_warn_about_implementation.py new file mode 100644 index 000000000000..f9a27522dfd6 --- /dev/null +++ b/tests/deprecation/test_warn_about_implementation.py @@ -0,0 +1,231 @@ +import inspect +import sys +import warnings +from contextlib import contextmanager + +from django.test import SimpleTestCase +from django.utils.deprecation import warn_about_implementation +from django.views.decorators import csrf + + +class WarnAboutImplementationTests(SimpleTestCase): + @contextmanager + def assertWarnsAboutLine(self, category, message, *, offset): + caller_frame = inspect.currentframe().f_back.f_back + with self.assertWarnsMessage(category, message) as warning: + yield + self.assertEqual(warning.filename, caller_frame.f_code.co_filename) + self.assertEqual(warning.lineno, caller_frame.f_lineno + offset) + + def test_function(self): + def some_function(): + pass + + with self.assertWarnsAboutLine(Warning, "deprecated", offset=-3): + warn_about_implementation("deprecated", Warning, some_function) + + def test_class(self): + class SomeClass: + pass + + with self.assertWarnsAboutLine(Warning, "deprecated", offset=-3): + warn_about_implementation("deprecated", Warning, SomeClass) + + def test_class_init(self): + class SomeClass: + def __init__(self): + pass + + with self.assertWarnsAboutLine(Warning, "deprecated", offset=-3): + warn_about_implementation("deprecated", Warning, SomeClass.__init__) + + def test_method(self): + class SomeClass: + def some_method(self): + pass + + for case, target in [ + ("unbound", SomeClass.some_method), + ("bound", SomeClass().some_method), + ]: + with ( + self.subTest(case), + self.assertWarnsAboutLine(Warning, "deprecated", offset=-9), + ): + warn_about_implementation("deprecated", Warning, target) + + def test_subclass_method(self): + class BaseClass: + def some_method(self): + pass + + def issue_warning(self): + warn_about_implementation("deprecated", Warning, self.some_method) + + class SubClass(BaseClass): + def some_method(self): + pass + + with self.assertWarnsAboutLine(Warning, "deprecated", offset=-3): + SubClass().issue_warning() + + def test_my_own_method(self): + with self.assertWarnsAboutLine(Warning, "deprecated", offset=-1): + warn_about_implementation("deprecated", Warning, self.test_my_own_method) + + def test_classmethod(self): + class SomeClass: + @classmethod + def some_classmethod(cls): + pass + + for case, target in [ + ("unbound", SomeClass.some_classmethod), + ("bound", SomeClass().some_classmethod), + ]: + with ( + self.subTest(case), + self.assertWarnsAboutLine(Warning, "deprecated", offset=-10), + ): + warn_about_implementation("deprecated", Warning, target) + + def test_staticmethod(self): + class SomeClass: + @staticmethod + def some_staticmethod(): + pass + + with self.assertWarnsAboutLine(Warning, "deprecated", offset=-4): + warn_about_implementation( + "deprecated", Warning, SomeClass.some_staticmethod + ) + + def test_property(self): + class SomeClass: + @property + def some_property(self): + return None + + with self.assertWarnsAboutLine(Warning, "deprecated", offset=-4): + warn_about_implementation( + "deprecated", + Warning, + inspect.getattr_static(SomeClass(), "some_property"), + ) + + def test_decorated_function(self): + @csrf.csrf_exempt + @csrf.ensure_csrf_cookie + def some_view(request): + pass + + with self.assertWarnsAboutLine(Warning, "deprecated", offset=-5): + warn_about_implementation("deprecated", Warning, some_view) + + def test_decorated_method(self): + class SomeClass: + @csrf.csrf_exempt + @csrf.ensure_csrf_cookie + def some_method(self): + pass + + for case, target in [ + ("unbound", SomeClass.some_method), + ("bound", SomeClass().some_method), + ]: + with ( + self.subTest(case), + self.assertWarnsAboutLine(Warning, "deprecated", offset=-11), + ): + warn_about_implementation("deprecated", Warning, target) + + def test_decorated_staticmethod(self): + class SomeClass: + @staticmethod + @csrf.csrf_exempt + @csrf.ensure_csrf_cookie + def some_staticmethod(): + pass + + with self.assertWarnsAboutLine(Warning, "deprecated", offset=-6): + warn_about_implementation( + "deprecated", Warning, SomeClass.some_staticmethod + ) + + def test_no_source_file(self): + # When the source file can't be determined, falls back to warning about + # the caller. (Builtins like `dict` have no source file.) + with self.assertWarnsAboutLine(Warning, "deprecated", offset=1): + warn_about_implementation("deprecated", Warning, dict) + + def test_rejects_invalid_target(self): + msg = ( + "target must be a function, class, bound method, or unbound descriptor " + "(classmethod, staticmethod, or property)." + ) + for invalid in ["string", object(), 123, None]: + with self.subTest(target=invalid), self.assertRaisesMessage(TypeError, msg): + warn_about_implementation("deprecated", Warning, invalid) + + def test_respects_warning_registry(self): + def some_function(): + pass + + # The "default" action only works correctly when warn_explicit() is + # called with the correct module and warning registry. (This test must + # use "default" or "module", not "once". CPython has an undocumented + # optimization for "once" that uses a global registry.) + with warnings.catch_warnings(record=True, action="default") as captured: + warn_about_implementation("deprecated", Warning, some_function) + self.assertEqual(len(captured), 1) + + captured.clear() + warn_about_implementation("deprecated", Warning, some_function) + self.assertEqual(len(captured), 0) + + def test_creates_warning_registry_if_needed(self): + def some_function(): + pass + + module = sys.modules[some_function.__module__] + if hasattr(module, "__warningregistry__"): + old_registry = module.__warningregistry__ + del module.__warningregistry__ + else: + old_registry = None + + try: + with warnings.catch_warnings(record=True, action="default") as captured: + warn_about_implementation("deprecated", Warning, some_function) + warn_about_implementation("deprecated", Warning, some_function) + + self.assertEqual(len(captured), 1) + self.assertIsInstance(module.__warningregistry__, dict) + self.assertNotEqual(module.__warningregistry__, {}) + finally: + if old_registry is not None: + module.__warningregistry__ = old_registry + elif hasattr(module, "__warningregistry__"): + del module.__warningregistry__ + + def test_missing_module(self): + def some_function(): + pass + + some_function.__module__ = "not_a_module" + + with self.assertWarnsAboutLine(Warning, "deprecated", offset=-5): + warn_about_implementation("deprecated", Warning, some_function) + + def test_non_standard_module(self): + sys.modules["sealed_module"] = type("SealedModule", (), {"__slots__": ()})() + self.addCleanup(sys.modules.pop, "sealed_module") + + def some_function(): + pass + + some_function.__module__ = "sealed_module" + + with self.assertWarnsAboutLine(Warning, "deprecated", offset=2): + # Module is not inspectable, so the warning is issued generically. + warn_about_implementation("deprecated", Warning, some_function) diff --git a/tests/modeladmin/tests.py b/tests/modeladmin/tests.py index 6a3574653344..2b608954432b 100644 --- a/tests/modeladmin/tests.py +++ b/tests/modeladmin/tests.py @@ -1035,11 +1035,13 @@ def test_list_select_related_true_deprecated(self): ) # RemovedInDjango70Warning: # with self.assertRaisesMessage(ValueError, msg) - with self.assertWarnsMessage(RemovedInDjango70Warning, msg): + with self.assertWarnsMessage(RemovedInDjango70Warning, msg) as warning: class TestModelAdmin(ModelAdmin): list_select_related = True + self.assertEqual(warning.filename, __file__) + # RemovedInDjango70Warning: when the deprecation ends, remove. def test_list_select_related_true_deprecated_subclass(self): with ignore_warnings( @@ -1073,8 +1075,9 @@ def get_list_select_related(self, request): # RemovedInDjango70Warning: # with self.assertRaisesMessage(ValueError, msg) - with self.assertWarnsMessage(RemovedInDjango70Warning, msg): + with self.assertWarnsMessage(RemovedInDjango70Warning, msg) as warning: TestModelAdmin(Band, self.site).get_changelist_instance(request) + self.assertEqual(warning.filename, __file__) class ModelAdminPermissionTests(SimpleTestCase): diff --git a/tests/urlpatterns_reverse/middleware.py b/tests/urlpatterns_reverse/middleware.py index 5fc04056299a..9c8ed2807661 100644 --- a/tests/urlpatterns_reverse/middleware.py +++ b/tests/urlpatterns_reverse/middleware.py @@ -1,6 +1,6 @@ from django.http import HttpResponse, StreamingHttpResponse +from django.middleware import MiddlewareMixin from django.urls import reverse -from django.utils.deprecation import MiddlewareMixin from . import urlconf_inner