diff --git a/django/utils/deprecation.py b/django/utils/deprecation.py index d690dc5d56..fbfed51109 100644 --- a/django/utils/deprecation.py +++ b/django/utils/deprecation.py @@ -1,8 +1,12 @@ import inspect +import os import warnings from asgiref.sync import iscoroutinefunction, markcoroutinefunction, sync_to_async +import django +from django.utils.version import PY312 + class RemovedInDjango60Warning(DeprecationWarning): pass @@ -140,3 +144,25 @@ class MiddlewareMixin: thread_sensitive=True, )(request, response) return response + + +def adjust_stacklevel_for_warning(skip_file_prefixes): + def _get_non_django_stacklevel(): + django_path = os.path.dirname(django.__file__) + stacklevel = 1 + # Exclude current and nested function frames with [2:]. + for frame_info in inspect.stack()[2:]: + filename = os.path.abspath(frame_info.filename) + if not filename.startswith(django_path): + return stacklevel + stacklevel += 1 + return 1 + + if PY312: + return { + "skip_file_prefixes": ( + os.path.normpath(os.path.dirname(skip_file_prefixes)), + ) + } + else: + return {"stacklevel": _get_non_django_stacklevel()} diff --git a/docs/internals/contributing/writing-code/submitting-patches.txt b/docs/internals/contributing/writing-code/submitting-patches.txt index cac6848d04..6e052ca052 100644 --- a/docs/internals/contributing/writing-code/submitting-patches.txt +++ b/docs/internals/contributing/writing-code/submitting-patches.txt @@ -114,7 +114,7 @@ requirements: feature, the change should also contain documentation. When you think your work is ready to be reviewed, send :doc:`a GitHub pull -request `. +request `. If you can't send a pull request for some reason, you can also use patches in Trac. When using this style, follow these guidelines. @@ -221,7 +221,10 @@ previous behavior, or standalone items that are unnecessary or unused when the deprecation ends. For example:: import warnings - from django.utils.deprecation import RemovedInDjangoXXWarning + from django.utils.deprecation import ( + RemovedInDjangoXXWarning, + adjust_stacklevel_for_warning, + ) # RemovedInDjangoXXWarning. @@ -234,7 +237,7 @@ deprecation ends. For example:: warnings.warn( "foo() is deprecated.", category=RemovedInDjangoXXWarning, - stacklevel=2, + **adjust_stacklevel_for_warning(__file__), ) old_private_helper() ... diff --git a/tests/deprecation/tests.py b/tests/deprecation/tests.py index 66f6a4d922..64c953bf68 100644 --- a/tests/deprecation/tests.py +++ b/tests/deprecation/tests.py @@ -1,5 +1,9 @@ +import importlib.util +import os import warnings +from pathlib import Path +import django from django.test import SimpleTestCase from django.utils.deprecation import RemovedAfterNextVersionWarning, RenameMethodsBase @@ -178,3 +182,37 @@ class RenameMethodsTests(SimpleTestCase): self.assertTrue( issubclass(RemovedAfterNextVersionWarning, PendingDeprecationWarning) ) + + +class AdjustStacklevelForWarningTests(SimpleTestCase): + def test_warning_in_django_code(self): + self.django_path = Path(os.path.dirname(django.__file__)) + self.temp_file_path = self.django_path / "temp_adjust_stacklevel_for_warning.py" + + file_content = """ +import warnings +from django.utils.deprecation import adjust_stacklevel_for_warning + +def django_function(): + warnings.warn( + "This is a test warning from Django code.", + DeprecationWarning, + **adjust_stacklevel_for_warning(__file__), + ) + """ + self.temp_file_path.write_text(file_content) + + module_name = "temp_adjust_stacklevel_for_warning" + spec = importlib.util.spec_from_file_location( + module_name, str(self.temp_file_path) + ) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + + msg = "This is a test warning from Django code." + with self.assertWarnsMessage(DeprecationWarning, msg) as ctx: + module.django_function() + + self.assertEqual(ctx.filename, __file__) + + self.temp_file_path.unlink()