commit:     035ba56322b282d8ed66bd4fba1f6f057d3cf7e7
Author:     Brian Harring <ferringb <AT> gmail <DOT> com>
AuthorDate: Sat Nov 29 18:28:02 2025 +0000
Commit:     Brian Harring <ferringb <AT> gmail <DOT> com>
CommitDate: Sat Nov 29 18:32:56 2025 +0000
URL:        
https://gitweb.gentoo.org/proj/pkgcore/snakeoil.git/commit/?id=035ba563

feat(delayed): rebuild demand_compile_regexp as delayed.regexp, delete 
demandload

Folks wiped the usage- intentionally- but never bothered to wipe the
implementation.

Don't do that, especially with codebases this old- demandload did fairly
nasty things due to the time of creation making it not simple.

Mark anything you're gutting w/ deprecated and then follow through with
the wipe please.

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

 src/snakeoil/delayed/__init__.py |  12 ++
 src/snakeoil/demandload.py       | 334 ++-------------------------------------
 tests/test_delayed.py            |  15 ++
 tests/test_demandload.py         | 135 ++--------------
 4 files changed, 58 insertions(+), 438 deletions(-)

diff --git a/src/snakeoil/delayed/__init__.py b/src/snakeoil/delayed/__init__.py
new file mode 100644
index 0000000..3a469ca
--- /dev/null
+++ b/src/snakeoil/delayed/__init__.py
@@ -0,0 +1,12 @@
+__all__ = ("regexp",)
+
+import functools
+import re
+
+from ..obj import DelayedInstantiation
+
+
[email protected](re.compile)
+def regexp(pattern: str, flags: int = 0):
+    """Lazily compile a regexp; reify it only when it's needed"""
+    return DelayedInstantiation(re.Pattern, re.compile, pattern, flags)

diff --git a/src/snakeoil/demandload.py b/src/snakeoil/demandload.py
index b1d80ba..400200a 100644
--- a/src/snakeoil/demandload.py
+++ b/src/snakeoil/demandload.py
@@ -1,325 +1,23 @@
-"""Demand load things when used.
+__all__ = ("demand_compile_regexp",)
 
-This uses :py:func:`Placeholder` objects which create an actual object on
-first use and know how to replace themselves with that object, so
-there is no performance penalty after first use.
-
-This trick is *mostly* transparent, but there are a few things you
-have to be careful with:
-
- - You may not bind a second name to a placeholder object. Specifically,
-   if you demandload C{bar} in module C{foo}, you may not
-   C{from foo import bar} in a third module. The placeholder object
-   does not "know" it gets imported, so this does not trigger the
-   demandload: C{bar} in the third module is the placeholder object.
-   When that placeholder gets used it replaces itself with the actual
-   module in C{foo} but not in the third module.
-   Because this is normally unwanted (it introduces a small
-   performance hit) the placeholder object will raise an exception if
-   it detects this. But if the demandload gets triggered before the
-   third module is imported you do not get that exception, so you
-   have to be careful not to import or otherwise pass around the
-   placeholder object without triggering it.
- - You may not demandload more than one level of lookups.  Specifically,
-   demandload("os.path") is not allowed- this would require a "os" fake
-   object in the local scope, one which would have a "path" fake object
-   pushed into the os object.  This is effectively not much of a limitation;
-   you can instead just lazyload 'path' directly via demandload("os:path").
- - Not all operations on the placeholder object trigger demandload.
-   The most common problem is that C{except ExceptionClass} does not
-   work if C{ExceptionClass} is a placeholder.
-   C{except module.ExceptionClass} with C{module} a placeholder does
-   work. You can normally avoid this by always demandloading the module, not
-   something in it. Another similar case is that C{isinstance Class} or
-   C{issubclass Class} does not work for the initial call since the proper
-   class hasn't replaced the placeholder until after the call. So the first
-   call will always return False with subsequent calls working as expected. The
-   previously mentioned workaround of demandloading the module works in this
-   case as well.
-"""
-
-__all__ = ("demandload", "demand_compile_regexp")
-
-import functools
-import os
+import re
 import sys
-import threading
-
-from .modules import load_any
-
-# There are some demandloaded imports below the definition of demandload.
-
-_allowed_chars = "".join(
-    (x.isalnum() or x in "_.") and " " or "a" for x in map(chr, range(256))
-)
-
-
-def parse_imports(imports):
-    """Parse a sequence of strings describing imports.
-
-    For every input string it returns a tuple of (import, targetname).
-    Examples::
-
-      'foo' -> ('foo', 'foo')
-      'foo:bar' -> ('foo.bar', 'bar')
-      'foo:bar,baz@spork' -> ('foo.bar', 'bar'), ('foo.baz', 'spork')
-      'foo@bar' -> ('foo', 'bar')
-
-    Notice 'foo.bar' is not a valid input.  Supporting 'foo.bar' would
-    result in nested demandloaded objects- this isn't desirable for
-    client code.  Instead use 'foo:bar'.
-
-    :type imports: sequence of C{str} objects.
-    :rtype: iterable of tuples of two C{str} objects.
-    """
-    for s in imports:
-        fromlist = s.split(":", 1)
-        if len(fromlist) == 1:
-            # Not a "from" import.
-            if "." in s:
-                raise ValueError(
-                    "dotted imports are disallowed; see "
-                    "snakeoil.demandload docstring for "
-                    f"details; {s!r}"
-                )
-            split = s.split("@", 1)
-            for s in split:
-                if not s.translate(_allowed_chars).isspace():
-                    raise ValueError(f"bad target: {s}")
-            if len(split) == 2:
-                yield tuple(split)
-            else:
-                split = split[0]
-                yield split, split
-        else:
-            # "from" import.
-            base, targets = fromlist
-            if not base.translate(_allowed_chars).isspace():
-                raise ValueError(f"bad target: {base}")
-            for target in targets.split(","):
-                split = target.split("@", 1)
-                for s in split:
-                    if not s.translate(_allowed_chars).isspace():
-                        raise ValueError(f"bad target: {s}")
-                yield base + "." + split[0], split[-1]
-
-
-def _protection_enabled_disabled():
-    return False
-
-
-def _noisy_protection_disabled():
-    return False
-
-
-def _protection_enabled_enabled():
-    val = os.environ.get("SNAKEOIL_DEMANDLOAD_PROTECTION", "n").lower()
-    return val in ("yes", "true", "1", "y")
-
-
-def _noisy_protection_enabled():
-    val = os.environ.get("SNAKEOIL_DEMANDLOAD_WARN", "y").lower()
-    return val in ("yes", "true", "1", "y")
-
-
-if "pydoc" in sys.modules or "epydoc" in sys.modules:
-    _protection_enabled = _protection_enabled_disabled
-    _noisy_protection = _noisy_protection_disabled
-else:
-    _protection_enabled = _protection_enabled_enabled
-    _noisy_protection = _noisy_protection_enabled
-
-
-class Placeholder:
-    """Object that knows how to replace itself when first accessed.
-
-    See the module docstring for common problems with its use.
-    """
-
-    @classmethod
-    def load_namespace(cls, scope, name, target):
-        """Object that imports modules into scope when first used.
-
-        See the module docstring for common problems with its use; used by
-        :py:func:`demandload`.
-        """
-        if not isinstance(target, str):
-            raise TypeError(f"Asked to load non string namespace: {target!r}")
-        return cls(scope, name, functools.partial(load_any, target))
-
-    @classmethod
-    def load_regex(cls, scope, name, *args, **kwargs):
-        """
-        Compiled Regex object that knows how to replace itself when first 
accessed.
+import typing
 
-        See the module docstring for common problems with its use; used by
-        :py:func:`demand_compile_regexp`.
-        """
-        if not args and not kwargs:
-            raise TypeError("re.compile requires at least one arg or kwargs")
-        return cls(scope, name, functools.partial(re.compile, *args, **kwargs))
+from .delayed import regexp
+from .deprecation import deprecated
 
-    def __init__(self, scope, name, load_func):
-        """Initialize.
 
-        :param scope: the scope we live in, normally the global namespace of
-            the caller (C{globals()}).
-        :param name: the name we have in C{scope}.
-        :param load_func: a functor that when invoked with no args, returns the
-            object we're demandloading.
-        """
-        if not callable(load_func):
-            raise TypeError(f"load_func must be callable; got {load_func!r}")
-        object.__setattr__(self, "_scope", scope)
-        object.__setattr__(self, "_name", name)
-        object.__setattr__(self, "_replacing_tids", [])
-        object.__setattr__(self, "_load_func", load_func)
-        object.__setattr__(self, "_loading_lock", threading.Lock())
+@deprecated("snakeoil.klass.demand_compile_regexp has moved to 
snakeoil.delayed.regexp")
+def demand_compile_regexp(
+    name: str, pattern: str, flags=0, /, scope: dict[str, typing.Any] | None = 
None
+) -> None:
+    """Lazily reify a re.compile.
 
-    def _target_already_loaded(self, complain=True):
-        name = object.__getattribute__(self, "_name")
-        scope = object.__getattribute__(self, "_scope")
-
-        # in a threaded environment, it's possible for tid1 to get the
-        # placeholder from globals, python switches to tid2, which triggers
-        # a full update (thus enabling this pathway), switch back to tid1,
-        # which then throws the complaint.
-        # this cannot be locked to address; the pull from global scope is
-        # what would need locking, and that's infeasible (VM shouldn't do it
-        # anyways; would kill performance)
-        # if threading is enabled, we'll have the tid's of the threads that
-        # triggered replacement; if the thread triggering this pathway isn't
-        # one of the ones that caused replacement, silence the warning.
-        # as for why we watch for the threading modules; if they're not there,
-        # it's impossible for this pathway to accidentally be triggered twice-
-        # meaning it is a misuse by the consuming client code.
-        if complain:
-            tids_to_complain_about = object.__getattribute__(self, 
"_replacing_tids")
-            if threading.current_thread().ident in tids_to_complain_about:
-                if _protection_enabled():
-                    raise ValueError(f"Placeholder for {name!r} was triggered 
twice")
-                elif _noisy_protection():
-                    logging.warning(
-                        "Placeholder for %r was triggered multiple times in 
file %r",
-                        name,
-                        scope.get("__file__", "unknown"),
-                    )
-        return scope[name]
-
-    def _get_target(self):
-        """Replace ourself in C{scope} with the result of our C{_load_func}.
-
-        :return: the result of calling C{_load_func}.
-        """
-        preloaded_func = object.__getattribute__(self, 
"_target_already_loaded")
-        with object.__getattribute__(self, "_loading_lock"):
-            load_func = object.__getattribute__(self, "_load_func")
-            if load_func is None:
-                # This means that there was contention; two threads made it 
into
-                # _get_target.  That's fine; suppress complaints, and return 
the
-                # preloaded value.
-                result = preloaded_func(False)
-            else:
-                # We're the first thread to try and do the load; load the 
target,
-                # fix the scope, and replace this method with one that 
shortcircuits
-                # (and appropriately complains) the lookup.
-                result = load_func()
-                scope = object.__getattribute__(self, "_scope")
-                name = object.__getattribute__(self, "_name")
-                scope[name] = result
-                # Replace this method with the fast path/preloaded one; this
-                # is to ensure complaints get leveled if needed.
-                object.__setattr__(self, "_get_target", preloaded_func)
-                object.__setattr__(self, "_load_func", None)
-
-            # note this step *has* to follow scope modification; else it
-            # will go maximum depth recursion.
-            tids = object.__getattribute__(self, "_replacing_tids")
-            tids.append(threading.current_thread().ident)
-
-        return result
-
-    def _load_func(self):
-        raise NotImplementedError
-
-    # Various methods proxied to our replacement.
-
-    def __str__(self):
-        return self.__getattribute__("__str__")()
-
-    def __getattribute__(self, attr):
-        result = object.__getattribute__(self, "_get_target")()
-        return getattr(result, attr)
-
-    def __setattr__(self, attr, value):
-        result = object.__getattribute__(self, "_get_target")()
-        setattr(result, attr, value)
-
-    def __call__(self, *args, **kwargs):
-        result = object.__getattribute__(self, "_get_target")()
-        return result(*args, **kwargs)
-
-
-def demandload(*imports, **kwargs):
-    """Import modules into the caller's global namespace when each is first 
used.
-
-    Other args are strings listing module names.
-    names are handled like this::
-
-        foo            import foo
-        foo@bar        import foo as bar
-        foo:bar        from foo import bar
-        foo:bar,quux   from foo import bar, quux
-        foo.bar:quux   from foo.bar import quux
-        foo:baz@quux   from foo import baz as quux
+    The mechanism of injecting into the scope is deprecated; move to 
snakeoil.delayed.regexp.
     """
-
-    # pull the caller's global namespace if undefined
-    scope = kwargs.pop("scope", sys._getframe(1).f_globals)
-
-    for source, target in parse_imports(imports):
-        scope[target] = Placeholder.load_namespace(scope, target, source)
-
-
-# Extra name to make undoing monkeypatching demandload with
-# disabled_demandload easier.
-enabled_demandload = demandload
-
-
-def disabled_demandload(*imports, **kwargs):
-    """Exactly like :py:func:`demandload` but does all imports immediately."""
-    scope = kwargs.pop("scope", sys._getframe(1).f_globals)
-    for source, target in parse_imports(imports):
-        scope[target] = load_any(source)
-
-
-def demand_compile_regexp(name, *args, **kwargs):
-    """Demandloaded version of :py:func:`re.compile`.
-
-    Extra arguments are passed unchanged to :py:func:`re.compile`.
-
-    :param name: the name of the compiled re object in that scope.
-    """
-    scope = kwargs.pop("scope", sys._getframe(1).f_globals)
-    scope[name] = Placeholder.load_regex(scope, name, *args, **kwargs)
-
-
-def disabled_demand_compile_regexp(name, *args, **kwargs):
-    """Exactly like :py:func:`demand_compile_regexp` but does all imports 
immediately."""
-    scope = kwargs.pop("scope", sys._getframe(1).f_globals)
-    scope[name] = re.compile(*args, **kwargs)
-
-
-if os.environ.get("SNAKEOIL_DEMANDLOAD_DISABLED", "n").lower() in (
-    "y",
-    "yes",
-    "1",
-    "true",
-):
-    demandload = disabled_demandload
-    demand_compile_regexp = disabled_demand_compile_regexp
-
-demandload(
-    "logging",
-    "re",
-)
+    if scope is None:
+        # Note 2, not 1- we're wrapped in deprecated so we *are* two levels in.
+        scope = sys._getframe(2).f_globals
+    delayed = regexp(pattern, flags)
+    scope[name] = delayed

diff --git a/tests/test_delayed.py b/tests/test_delayed.py
new file mode 100644
index 0000000..c74f454
--- /dev/null
+++ b/tests/test_delayed.py
@@ -0,0 +1,15 @@
+import re
+
+from snakeoil import delayed
+
+
+def test_regexp():
+    d = delayed.regexp("aasdf", 1)
+    assert re.Pattern is not type(d), "a proxy wasn't returned"
+    assert "aasdf" == d.pattern
+    assert re.compile("asdf", 1).flags == d.flags
+    assert d.match("aasdf")
+    assert re.compile("fdas").flags == delayed.regexp("").flags
+
+    # assert we lie.
+    assert isinstance(delayed.regexp("asdf"), re.Pattern)

diff --git a/tests/test_demandload.py b/tests/test_demandload.py
index 0ef6d9a..12daf1a 100644
--- a/tests/test_demandload.py
+++ b/tests/test_demandload.py
@@ -2,127 +2,22 @@ import re
 
 import pytest
 
-from snakeoil import demandload
-
-# few notes:
-# all tests need to be wrapped w/ the following decorator; it
-# ensures that snakeoils env-aware disabling is reversed, ensuring the
-# setup is what the test expects.
-# it also explicitly resets the state on the way out.
-
-
-def reset_globals(functor):
-    def f(*args, **kwds):
-        orig_demandload = demandload.demandload
-        orig_demand_compile = demandload.demand_compile_regexp
-        orig_protection = demandload._protection_enabled
-        orig_noisy = demandload._noisy_protection
-        try:
-            return functor(*args, **kwds)
-        finally:
-            demandload.demandload = orig_demandload
-            demandload.demand_compile_regexp = orig_demand_compile
-            demandload._protection_enabled = orig_protection
-            demandload._noisy_protection = orig_noisy
-
-    return f
-
-
-class TestParser:
-    @reset_globals
-    def test_parse(self):
-        for input, output in [
-            ("foo", [("foo", "foo")]),
-            ("foo:bar", [("foo.bar", "bar")]),
-            ("foo:bar,baz@spork", [("foo.bar", "bar"), ("foo.baz", "spork")]),
-            ("foo@bar", [("foo", "bar")]),
-            ("foo_bar", [("foo_bar", "foo_bar")]),
-        ]:
-            assert output == list(demandload.parse_imports([input]))
-        pytest.raises(ValueError, list, demandload.parse_imports(["a.b"]))
-        pytest.raises(ValueError, list, demandload.parse_imports(["a:,"]))
-        pytest.raises(ValueError, list, demandload.parse_imports(["a:b,x@"]))
-        pytest.raises(ValueError, list, demandload.parse_imports(["b-x"]))
-        pytest.raises(ValueError, list, demandload.parse_imports([" b_x"]))
-
-
-class TestPlaceholder:
-    @reset_globals
-    def test_getattr(self):
-        scope = {}
-        placeholder = demandload.Placeholder(scope, "foo", list)
-        assert scope == object.__getattribute__(placeholder, "_scope")
-        assert placeholder.__doc__ == [].__doc__
-        assert scope["foo"] == []
-        demandload._protection_enabled = lambda: True
-        with pytest.raises(ValueError):
-            getattr(placeholder, "__doc__")
-
-    @reset_globals
-    def test__str__(self):
-        scope = {}
-        placeholder = demandload.Placeholder(scope, "foo", list)
-        assert scope == object.__getattribute__(placeholder, "_scope")
-        assert str(placeholder) == str([])
-        assert scope["foo"] == []
-
-    @reset_globals
-    def test_call(self):
-        def passthrough(*args, **kwargs):
-            return args, kwargs
-
-        def get_func():
-            return passthrough
-
-        scope = {}
-        placeholder = demandload.Placeholder(scope, "foo", get_func)
-        assert scope == object.__getattribute__(placeholder, "_scope")
-        assert (("arg",), {"kwarg": 42}) == placeholder("arg", kwarg=42)
-        assert passthrough is scope["foo"]
-
-    @reset_globals
-    def test_setattr(self):
-        class Struct:
-            pass
-
-        scope = {}
-        placeholder = demandload.Placeholder(scope, "foo", Struct)
-        placeholder.val = 7
-        demandload._protection_enabled = lambda: True
-        with pytest.raises(ValueError):
-            getattr(placeholder, "val")
-        assert 7 == scope["foo"].val
-
-
-class TestImport:
-    @reset_globals
-    def test_demandload(self):
-        scope = {}
-        demandload.demandload("snakeoil:demandload", scope=scope)
-        assert demandload is not scope["demandload"]
-        assert demandload.demandload is scope["demandload"].demandload
-        assert demandload is scope["demandload"]
-
-    @reset_globals
-    def test_disabled_demandload(self):
-        scope = {}
-        demandload.disabled_demandload("snakeoil:demandload", scope=scope)
-        assert demandload is scope["demandload"]
+from snakeoil import demandload, deprecation
 
 
 class TestDemandCompileRegexp:
-    @reset_globals
     def test_demand_compile_regexp(self):
-        scope = {}
-        demandload.demand_compile_regexp("foo", "frob", scope=scope)
-        assert list(scope.keys()) == ["foo"]
-        assert "frob" == scope["foo"].pattern
-        assert "frob" == scope["foo"].pattern
-
-        # verify it's delayed via a bad regex.
-        demandload.demand_compile_regexp("foo", "f(", scope=scope)
-        assert list(scope.keys()) == ["foo"]
-        # should blow up on accessing an attribute.
-        obj = scope["foo"]
-        with pytest.raises(re.error):
-            getattr(obj, "pattern")
+        with deprecation.suppress_deprecation_warning():
+            scope = {}
+            demandload.demand_compile_regexp("foo", "frob", scope=scope)
+            assert list(scope.keys()) == ["foo"]
+            assert "frob" == scope["foo"].pattern
+            assert "frob" == scope["foo"].pattern
+
+            # verify it's delayed via a bad regex.
+            demandload.demand_compile_regexp("foo", "f(", scope=scope)
+            assert list(scope.keys()) == ["foo"]
+            # should blow up on accessing an attribute.
+            obj = scope["foo"]
+            with pytest.raises(re.error):
+                getattr(obj, "pattern")

Reply via email to