commit:     bc1955a8e5505c0a1c5172395c052b70d32bb35e
Author:     Brian Harring <ferringb <AT> gmail <DOT> com>
AuthorDate: Sat Jan 10 10:13:32 2026 +0000
Commit:     Brian Harring <ferringb <AT> gmail <DOT> com>
CommitDate: Sat Jan 10 10:15:43 2026 +0000
URL:        
https://gitweb.gentoo.org/proj/pkgcore/snakeoil.git/commit/?id=bc1955a8

mark protect_imports as just for tests

protect_imports is not thread safe and shouldn't be used in actual
runtime code; the python_namespaces module now provides other
ways to do thread safe imports of modules outside of sys.path
which address what this was used for.

This is left in place for tests since that scenario is always
thread safe for our sync code, and the shorthand is useful.

Signed-off-by: Brian Harring <ferringb <AT> gmail.com>

 src/snakeoil/delayed/__init__.py     | 23 ++++++++++++++---
 src/snakeoil/deprecation/registry.py |  9 ++++++-
 src/snakeoil/python_namespaces.py    | 49 ++++++++++++++----------------------
 src/snakeoil/test/__init__.py        | 33 ++++++++++++++++++++++++
 tests/test_delayed.py                |  5 ++++
 5 files changed, 84 insertions(+), 35 deletions(-)

diff --git a/src/snakeoil/delayed/__init__.py b/src/snakeoil/delayed/__init__.py
index 8feb83c..1599bb4 100644
--- a/src/snakeoil/delayed/__init__.py
+++ b/src/snakeoil/delayed/__init__.py
@@ -1,11 +1,13 @@
-__all__ = ("regexp",)
+__all__ = ("regexp", "import_module", "is_delayed")
 
 import functools
 import importlib
 import re
+import sys
 import types
+import typing
 
-from ..obj import DelayedInstantiation
+from ..obj import BaseDelayedObject, DelayedInstantiation
 
 
 @functools.wraps(re.compile)
@@ -14,6 +16,19 @@ def regexp(pattern: str, flags: int = 0):
     return DelayedInstantiation(re.Pattern, re.compile, pattern, flags)
 
 
-def import_module(target: str) -> types.ModuleType:
-    """Import a module at time of access.  This is a shim for python's lazy 
import in 3.15"""
+def import_module(target: str, force_proxy=False) -> types.ModuleType:
+    """Import a module at time of access if it's not already imported.  This 
is a shim for python's lazy import in 3.15
+
+    :param target: the python namespace path of what to import.  
`snakeoil.klass` for example.
+    :param force_proxy: Even if the module is in sys.modules, still return a 
proxy.  This is a break glass
+      control only relevant for hard cycle breaking.
+    """
+    if not force_proxy and (module := sys.modules.get(target, None)) is not 
None:
+        return module
     return DelayedInstantiation(types.ModuleType, importlib.import_module, 
target)
+
+
+# Convert this to a type guard when py3.14 is min.
+def is_delayed(obj: typing.Any) -> bool:
+    cls = object.__getattribute__(obj, "__class__")
+    return isinstance(cls, BaseDelayedObject)

diff --git a/src/snakeoil/deprecation/registry.py 
b/src/snakeoil/deprecation/registry.py
index 26c461c..5628636 100644
--- a/src/snakeoil/deprecation/registry.py
+++ b/src/snakeoil/deprecation/registry.py
@@ -3,7 +3,7 @@ import sys
 import typing
 import warnings
 
-from ..delayed import import_module
+from ..delayed import import_module, is_delayed
 from .util import suppress_deprecations
 
 python_namespaces = import_module("snakeoil.python_namespaces")
@@ -137,6 +137,13 @@ class Registry:
         """Decorate a callable with a deprecation notice, registering it in 
the internal list of deprecations"""
 
         def f(functor):
+            # Catch mistakes that force proxy objects to realize immediately
+            if is_delayed(functor):
+                raise ValueError(
+                    "deprecation of lazy instantiation objects 
(`snakeoil.obj.DelayedInstantation` for example) are not possible.  
`warnings.deprecated` will immediately reify them.  "
+                    "You must interpose a *real* functor that internally 
invokes the delayed object when accessed; this is the only way to shield the 
delayed object `from warnings.deprecated` triggering it."
+                )
+
             if not self.is_enabled:
                 return functor
 

diff --git a/src/snakeoil/python_namespaces.py 
b/src/snakeoil/python_namespaces.py
index 7f0e7dc..745e407 100644
--- a/src/snakeoil/python_namespaces.py
+++ b/src/snakeoil/python_namespaces.py
@@ -1,16 +1,32 @@
 __all__ = ("import_submodules_of", "get_submodules_of")
 
-import contextlib
 import os
-import sys
 import types
 import typing
-from importlib import import_module, invalidate_caches, machinery
+from importlib import import_module, machinery
 from importlib import util as import_util
 from pathlib import Path
 
+from . import delayed
+from ._internals import deprecated
+
 T_class_filter = typing.Callable[[str], bool]
 
+# Delayed to avoid triggering all test crap- pytest for example- and cycle 
breaking
+# while the deprecation is being moved out.  There is a cycle for all of this
+# that is due to klass needing the deprecated registry whilst having it's own 
deprecations.
+_test = delayed.import_module("snakeoil.test")
+
+
+@deprecated(
+    "use `snakeoil.tests.protect_imports`; this should only be used for tests. 
 Runtime usage should use `import_module_from_path`",
+    removal_in=(0, 12, 0),
+    qualname="snakeoil.python_namespaces.protect_imports",
+)
+def protect_imports():
+    # isolate the access so only when this is invoked, does _test reify.
+    return _test.protect_imports()
+
 
 def get_submodules_of(
     root: types.ModuleType | str,
@@ -111,33 +127,6 @@ def remove_py_extension(path: Path | str) -> str | None:
     return None
 
 
[email protected]
-def protect_imports() -> typing.Generator[
-    tuple[list[str], dict[str, types.ModuleType]], None, None
-]:
-    """
-    Non threadsafe mock.patch of internal imports to allow revision
-
-    This should used in tests or very select scenarios.  Assume that underlying
-    c extensions that hold internal static state (curse module) will reimport, 
but
-    will not be 'clean'.  Any changes an import inflicts on the other modules 
in
-    memory, etc, this cannot block that.  Nor is this intended to do so; it's
-    for controlled tests or very specific usages.
-    """
-    orig_content = sys.path[:]
-    orig_modules = sys.modules.copy()
-    with contextlib.nullcontext():
-        yield sys.path, sys.modules
-
-    sys.path[:] = orig_content
-    # This is explicitly not thread safe, but manipulating sys.path 
fundamentally isn't thus this context
-    # isn't thread safe.  TL;dr: nuke it, and restore, it's the only way to be 
sure (to paraphrase)
-    sys.modules.clear()
-    sys.modules.update(orig_modules)
-    # Out of paranoia, force loaders to reset their caches.
-    invalidate_caches()
-
-
 def import_module_from_path(
     path: str | Path, module_name: str | None = None
 ) -> types.ModuleType:

diff --git a/src/snakeoil/test/__init__.py b/src/snakeoil/test/__init__.py
index 1539552..e97510c 100644
--- a/src/snakeoil/test/__init__.py
+++ b/src/snakeoil/test/__init__.py
@@ -7,16 +7,21 @@ __all__ = (
     "Modules",
     "NamespaceCollector",
     "protect_process",
+    "protect_imports",
     "random_str",
     "Slots",
 )
 
 
+import contextlib
 import os
 import random
 import string
 import subprocess
 import sys
+import types
+import typing
+from importlib import invalidate_caches
 from unittest.mock import patch
 
 from .abstract import AbstractTest
@@ -113,3 +118,31 @@ def hide_imports(*import_names: str):
         return orig_import(name, *args, **kwargs)
 
     return patch("builtins.__import__", side_effect=mock_import)
+
+
[email protected]
+def protect_imports() -> typing.Generator[
+    tuple[list[str], dict[str, types.ModuleType]], None, None
+]:
+    """
+    Non threadsafe mock.patch of internal imports to allow revision
+
+    This should used in tests or very select scenarios.  Assume that underlying
+    c extensions that hold internal static state (curse module) will reimport, 
but
+    will not be 'clean'.  Any changes an import inflicts on the other modules 
in
+    memory, etc, this cannot block that.  Nor is this intended to do so; it's
+    for controlled tests or very specific usages.
+    """
+    # Do not change this code without changing 
python_namespaces.protect_imports.  We have two implementations due to cycle 
issues.
+    orig_content = sys.path[:]
+    orig_modules = sys.modules.copy()
+    with contextlib.nullcontext():
+        yield sys.path, sys.modules
+
+    sys.path[:] = orig_content
+    # This is explicitly not thread safe, but manipulating sys.path 
fundamentally isn't thus this context
+    # isn't thread safe.  TL;dr: nuke it, and restore, it's the only way to be 
sure (to paraphrase)
+    sys.modules.clear()
+    sys.modules.update(orig_modules)
+    # Out of paranoia, force loaders to reset their caches.
+    invalidate_caches()

diff --git a/tests/test_delayed.py b/tests/test_delayed.py
index 82e4fd7..5f57399 100644
--- a/tests/test_delayed.py
+++ b/tests/test_delayed.py
@@ -27,3 +27,8 @@ def test_import_module(tmp_path):
         assert "blah" in modules
         assert 1 == f.x
         assert modules["blah"] is not f
+
+        shortcircuited = delayed.import_module("blah")
+        assert modules["blah"] is shortcircuited, (
+            "import_module must return the module if it already is in 
sys.modules rather than a proxy"
+        )

Reply via email to