commit: 1ea9cc91920e00ecf1cd8544a0dbc981e20968bd
Author: Brian Harring <ferringb <AT> gmail <DOT> com>
AuthorDate: Wed Dec 10 11:09:47 2025 +0000
Commit: Brian Harring <ferringb <AT> gmail <DOT> com>
CommitDate: Wed Dec 10 11:10:52 2025 +0000
URL:
https://gitweb.gentoo.org/proj/pkgcore/snakeoil.git/commit/?id=1ea9cc91
chore: deprecation.Registry.module() to deprecate a module
This can only flag the first import, but that's sufficient.
Signed-off-by: Brian Harring <ferringb <AT> gmail.com>
src/snakeoil/deprecation.py | 35 ++++++++++++++++++++++++++++++++++-
tests/test_deprecation.py | 42 +++++++++++++++++++++++++++++++++++++++++-
2 files changed, 75 insertions(+), 2 deletions(-)
diff --git a/src/snakeoil/deprecation.py b/src/snakeoil/deprecation.py
index ff27e79..380cbb6 100644
--- a/src/snakeoil/deprecation.py
+++ b/src/snakeoil/deprecation.py
@@ -64,6 +64,15 @@ class RecordCallable(Record):
yield from super(RecordCallable, self)._collect_strings()
[email protected](slots=True, frozen=True, kw_only=True)
+class RecordModule(Record):
+ qualname: str
+
+ def _collect_strings(self) -> typing.Iterator[str]:
+ yield f"module={self.qualname!r}"
+ yield from super(RecordModule, self)._collect_strings()
+
+
# When py3.13 is the min, add a defaulted generic of Record in this, and
# deprecated the init record_class argument.
class Registry:
@@ -93,7 +102,7 @@ class Registry:
stacklevel: typing.ClassVar[int] = 1 if is_enabled else 0
if is_enabled:
- from warnings import deprecated as _deprecated_callable
+ _deprecated_callable = warnings.deprecated
def __init__(
self, project: str, /, *, record_class: type[RecordCallable] =
RecordCallable
@@ -161,6 +170,30 @@ class Registry:
Record(msg=msg, removal_in=removal_in, removal_in_py=removal_in_py)
)
+ def module(
+ self,
+ msg: str,
+ qualname: str,
+ removal_in: Version | None = None,
+ removal_in_py: Version | None = None,
+ ) -> None:
+ """Deprecation notice that fires for the first import of this
module."""
+ if not self.is_enabled:
+ return
+ self._deprecations.append(
+ r := RecordModule(
+ msg,
+ qualname=qualname,
+ removal_in=removal_in,
+ removal_in_py=removal_in_py,
+ )
+ )
+ # fire the warning; we're triggering it a frame deep from the actual
issue (the module itself), thus adjust the stack level
+ # to skip us, the module defining the deprecation, and hit the import
directly.
+ warnings.warn(
+ str(r), category=DeprecationWarning, stacklevel=self.stacklevel + 2
+ )
+
@staticmethod
@contextlib.contextmanager
def suppress_deprecations(
diff --git a/tests/test_deprecation.py b/tests/test_deprecation.py
index a409e04..33d55f3 100644
--- a/tests/test_deprecation.py
+++ b/tests/test_deprecation.py
@@ -1,10 +1,19 @@
import dataclasses
import sys
import warnings
+from pathlib import Path
+from textwrap import dedent
import pytest
-from snakeoil.deprecation import Record, RecordCallable, Registry,
suppress_deprecations
+from snakeoil.deprecation import (
+ Record,
+ RecordCallable,
+ RecordModule,
+ Registry,
+ suppress_deprecations,
+)
+from snakeoil.python_namespaces import protect_imports
requires_enabled = pytest.mark.skipif(
not Registry.is_enabled, reason="requires python >=3.13.0"
@@ -155,6 +164,37 @@ class TestRegistry:
Record("asdf", removal_in=(1, 0, 0), removal_in_py=(2, 0, 0)) ==
list(r)[0]
)
+ @requires_enabled
+ def test_module(self, tmpdir):
+ with (tmpdir / "deprecated_import.py").open("w") as f:
+ f.write("import this_is_deprecated")
+ with (tmpdir / "this_is_deprecated.py").open("w") as f:
+ f.write(
+ dedent(
+ """
+ from snakeoil.deprecation import Registry
+ Registry("test").module('deprecation test', 'this_is_deprecated')
+ """
+ )
+ )
+
+ with protect_imports() as (paths, _):
+ paths.append(str(tmpdir))
+ with pytest.warns() as captures:
+ import deprecated_import # pyright:
ignore[reportMissingImports]
+
+ assert 1 == len(captures)
+ w = captures[0]
+ assert "deprecation test" in str(w)
+ assert w.filename.endswith("/deprecated_import.py")
+ assert 1 == w.lineno
+
+
+def test_RecordModule_str():
+ assert "module='foon.blah', removal in python=3.0.2, reason: why not" ==
str(
+ RecordModule("why not", qualname="foon.blah", removal_in_py=(3, 0, 2))
+ )
+
def test_Record_str():
assert "removal in version=1.0.2, removal in python=3.0.2, reason: blah"
== str(