From 2cee04008322550abff89ac9a0f1ac059358554b Mon Sep 17 00:00:00 2001 From: "Philipp A." Date: Fri, 4 Sep 2026 15:53:18 +0200 Subject: [PATCH 1/4] feat: add `skip_file_prefixes` to `warnings.deprecated` --- Doc/library/warnings.rst | 15 ++++++++++----- Lib/_py_warnings.py | 21 +++++++++++++-------- Lib/test/test_warnings/__init__.py | 19 +++++++++++++++++++ 3 files changed, 42 insertions(+), 13 deletions(-) diff --git a/Doc/library/warnings.rst b/Doc/library/warnings.rst index d5f89102d5853bb..781a0b98a1289da 100644 --- a/Doc/library/warnings.rst +++ b/Doc/library/warnings.rst @@ -566,7 +566,7 @@ Available Functions and calls to :func:`simplefilter`. -.. decorator:: deprecated(message, /, *, category=DeprecationWarning, stacklevel=1) +.. decorator:: deprecated(message, /, *, category=DeprecationWarning, stacklevel=1, skip_file_prefixes=()) Decorator to indicate that a class, function or overload is deprecated. @@ -598,12 +598,14 @@ Available Functions on use of deprecated objects. For functions, that happens on calls; for classes, on instantiation and on creation of subclasses. If the *category* is ``None``, no warning is emitted at runtime. - The *stacklevel* determines where the - warning is emitted. If it is ``1`` (the default), the warning + The *stacklevel* and *skip_file_prefixes* entries determine where the + warning is emitted. If *stacklevel* is ``1`` (the default), the warning is emitted at the direct caller of the deprecated object; if it is higher, it is emitted further up the stack. - Static type checker behavior is not affected by the *category* - and *stacklevel* arguments. + Frames in files whose path starts with any of the strings in + *skip_file_prefixes* are skipped. + Static type checker behavior is not affected by the *category*, + *skip_file_prefixes*, and *stacklevel* arguments. The deprecation message passed to the decorator is saved in the ``__deprecated__`` attribute on the decorated object. @@ -615,6 +617,9 @@ Available Functions .. versionadded:: 3.13 See :pep:`702`. + .. versionchanged:: 3.16 + Added *skip_file_prefixes*. + Available Context Managers -------------------------- diff --git a/Lib/_py_warnings.py b/Lib/_py_warnings.py index ab09913de6812dd..81d55cb0bda186c 100644 --- a/Lib/_py_warnings.py +++ b/Lib/_py_warnings.py @@ -3,6 +3,7 @@ import sys import _contextvars import _thread +lazy from collections.abc import Iterable __all__ = ["warn", "warn_explicit", "showwarning", @@ -763,12 +764,14 @@ def g(x: str) -> int: ... on use of deprecated objects. For functions, that happens on calls; for classes, on instantiation and on creation of subclasses. If the *category* is ``None``, no warning is emitted at runtime. - The *stacklevel* determines where the - warning is emitted. If it is ``1`` (the default), the warning + The *stacklevel* and *skip_file_prefixes* entries determine where the + warning is emitted. If *stacklevel* is ``1`` (the default), the warning is emitted at the direct caller of the deprecated object; if it is higher, it is emitted further up the stack. - Static type checker behavior is not affected by the *category* - and *stacklevel* arguments. + Frames in files whose path starts with any of the strings in + *skip_file_prefixes* are skipped. + Static type checker behavior is not affected by the *category*, + *skip_file_prefixes*, and *stacklevel* arguments. The deprecation message passed to the decorator is saved in the ``__deprecated__`` attribute on the decorated object. @@ -786,6 +789,7 @@ def __init__( *, category: type[Warning] | None = DeprecationWarning, stacklevel: int = 1, + skip_file_prefixes: Iterable[str] = (), ) -> None: if not isinstance(message, str): raise TypeError( @@ -794,6 +798,7 @@ def __init__( self.message = message self.category = category self.stacklevel = stacklevel + self.skip_file_prefixes = tuple(skip_file_prefixes) def __call__(self, arg, /): # Make sure the inner functions created below don't @@ -813,7 +818,7 @@ def __call__(self, arg, /): @functools.wraps(original_new) def __new__(cls, /, *args, **kwargs): if cls is arg: - _wm.warn(msg, category=category, stacklevel=stacklevel + 1) + _wm.warn(msg, category=category, stacklevel=stacklevel + 1, skip_file_prefixes=self.skip_file_prefixes) if original_new is not object.__new__: return original_new(cls, *args, **kwargs) # Mirrors a similar check in object.__new__. @@ -837,11 +842,11 @@ def __new__(cls, /, *args, **kwargs): @functools.wraps(original_init_subclass) def __init_subclass__(*args, **kwargs): - _wm.warn(msg, category=category, stacklevel=stacklevel + 1) + _wm.warn(msg, category=category, stacklevel=stacklevel + 1, skip_file_prefixes=self.skip_file_prefixes) return original_init_subclass(*args, **kwargs) else: def __init_subclass__(cls, *args, **kwargs): - _wm.warn(msg, category=category, stacklevel=stacklevel + 1) + _wm.warn(msg, category=category, stacklevel=stacklevel + 1, skip_file_prefixes=self.skip_file_prefixes) return super(arg, cls).__init_subclass__(*args, **kwargs) arg.__init_subclass__ = classmethod(__init_subclass__) @@ -855,7 +860,7 @@ def __init_subclass__(cls, *args, **kwargs): @functools.wraps(arg) def wrapper(*args, **kwargs): - _wm.warn(msg, category=category, stacklevel=stacklevel + 1) + _wm.warn(msg, category=category, stacklevel=stacklevel + 1, skip_file_prefixes=self.skip_file_prefixes) return arg(*args, **kwargs) if inspect.iscoroutinefunction(arg): diff --git a/Lib/test/test_warnings/__init__.py b/Lib/test/test_warnings/__init__.py index 5b5d5c2f70ffd7a..e0aff1f8dfb4edd 100644 --- a/Lib/test/test_warnings/__init__.py +++ b/Lib/test/test_warnings/__init__.py @@ -2252,6 +2252,25 @@ def c(): with self.assertWarnsRegex(RuntimeWarning, "c will go away soon"): c() + def test_skip_file_prefixes(): + code = """\ +from warnings import deprecated + +@deprecated("good as gone", skip_file_prefixes=(__file__,)) +def nested_func() -> Outer: + pass +""" + mod = types.ModuleType("mod") + mod.__file__ = "/fictional/path/my_test_file.py" + exec(code, mod.__dict__) + + with py_warnings.catch_warnings(record=True) as record: + py_warnings.simplefilter("always") + lineno_expected = sys._getframe().f_lineno + 1 # next line + mod.nested_func() + + assert (record[0].filename, record[0].lineno) == (__file__, lineno_expected) + def test_turn_off_warnings(self): @deprecated("d will go away soon", category=None) def d(): From 0a0c79785e362d7ad5635a47d3b35d88a72fb2af Mon Sep 17 00:00:00 2001 From: "blurb-it[bot]" <43283697+blurb-it[bot]@users.noreply.github.com> Date: Fri, 4 Sep 2026 14:06:38 +0000 Subject: [PATCH 2/4] =?UTF-8?q?=F0=9F=93=9C=F0=9F=A4=96=20Added=20by=20blu?= =?UTF-8?q?rb=5Fit.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../next/Library/2026-09-04-14-06-30.gh-issue-156929.6eTccv.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 Misc/NEWS.d/next/Library/2026-09-04-14-06-30.gh-issue-156929.6eTccv.rst diff --git a/Misc/NEWS.d/next/Library/2026-09-04-14-06-30.gh-issue-156929.6eTccv.rst b/Misc/NEWS.d/next/Library/2026-09-04-14-06-30.gh-issue-156929.6eTccv.rst new file mode 100644 index 000000000000000..578530be9f97dd2 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-09-04-14-06-30.gh-issue-156929.6eTccv.rst @@ -0,0 +1 @@ +Add ``skip_file_prefixes`` parameter to :deco:`warnings.deprecated``, mirroring the parameter of the same name for :func:`warnings.warn`. From 4caf4c4c5e991433ab463bad521cd882415e05e1 Mon Sep 17 00:00:00 2001 From: "Philipp A." Date: Fri, 4 Sep 2026 16:07:19 +0200 Subject: [PATCH 3/4] typo --- .../next/Library/2026-09-04-14-06-30.gh-issue-156929.6eTccv.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Misc/NEWS.d/next/Library/2026-09-04-14-06-30.gh-issue-156929.6eTccv.rst b/Misc/NEWS.d/next/Library/2026-09-04-14-06-30.gh-issue-156929.6eTccv.rst index 578530be9f97dd2..1ff7e43bfe5fbda 100644 --- a/Misc/NEWS.d/next/Library/2026-09-04-14-06-30.gh-issue-156929.6eTccv.rst +++ b/Misc/NEWS.d/next/Library/2026-09-04-14-06-30.gh-issue-156929.6eTccv.rst @@ -1 +1 @@ -Add ``skip_file_prefixes`` parameter to :deco:`warnings.deprecated``, mirroring the parameter of the same name for :func:`warnings.warn`. +Add ``skip_file_prefixes`` parameter to :deco:`warnings.deprecated`, mirroring the parameter of the same name for :func:`warnings.warn`. From 9a942d0ea6c31071d185ec777ea57aac682b26a7 Mon Sep 17 00:00:00 2001 From: "Philipp A." Date: Fri, 4 Sep 2026 16:57:02 +0200 Subject: [PATCH 4/4] fix --- Lib/test/test_warnings/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/test_warnings/__init__.py b/Lib/test/test_warnings/__init__.py index e0aff1f8dfb4edd..52c5b157d324380 100644 --- a/Lib/test/test_warnings/__init__.py +++ b/Lib/test/test_warnings/__init__.py @@ -2252,7 +2252,7 @@ def c(): with self.assertWarnsRegex(RuntimeWarning, "c will go away soon"): c() - def test_skip_file_prefixes(): + def test_skip_file_prefixes(self): code = """\ from warnings import deprecated