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(

Reply via email to