commit: 6d8238e215722385ae2781bd184eb88d02996e3e
Author: Brian Harring <ferringb <AT> gmail <DOT> com>
AuthorDate: Thu Oct 23 17:38:04 2025 +0000
Commit: Arthur Zamarin <arthurzam <AT> gentoo <DOT> org>
CommitDate: Fri Oct 24 08:56:10 2025 +0000
URL:
https://gitweb.gentoo.org/proj/pkgcore/snakeoil.git/commit/?id=6d8238e2
feat: make WeakInstMeta preserve __init__ annotations
For pydoc pkgcore.ebuild.atom, this results in this change:
before:
class atom(pkgcore.restrictions.boolean.AndRestriction)
| atom(*a, **kw)
after:
class atom(pkgcore.restrictions.boolean.AndRestriction)
| atom(atom: str, negate_vers: bool = False, eapi: str = '-1')
Whilst I'm in here, also clean up this code to use modern syntax
and be saner to read.
Signed-off-by: Brian Harring <ferringb <AT> gmail.com>
Closes: https://github.com/pkgcore/snakeoil/pull/106
Signed-off-by: Arthur Zamarin <arthurzam <AT> gentoo.org>
src/snakeoil/caching.py | 77 ++++++++++++++++++++++++++-----------------------
tests/test_caching.py | 20 +++++++++++++
2 files changed, 61 insertions(+), 36 deletions(-)
diff --git a/src/snakeoil/caching.py b/src/snakeoil/caching.py
index b20f074..ab3f2f3 100644
--- a/src/snakeoil/caching.py
+++ b/src/snakeoil/caching.py
@@ -54,6 +54,7 @@ Simple usage example:
__all__ = ("WeakInstMeta",)
+import functools
import warnings
from weakref import WeakValueDictionary
@@ -80,42 +81,46 @@ class WeakInstMeta(type):
U{pkgcore project<http://pkgcore.org>}
"""
- def __new__(cls, name, bases, d):
- if d.get("__inst_caching__", False):
- d["__inst_caching__"] = True
- d["__inst_dict__"] = WeakValueDictionary()
- else:
- d["__inst_caching__"] = False
- slots = d.get("__slots__")
- # get ourselves a singleton to be safe...
- o = object()
- if slots is not None:
- for base in bases:
- if getattr(base, "__weakref__", o) is not o:
- break
- else:
- d["__slots__"] = tuple(slots) + ("__weakref__",)
- return type.__new__(cls, name, bases, d)
+ def __new__(cls, name: str, bases: tuple[type, ...], scope) -> type:
+ if scope.setdefault("__inst_caching__", False):
+ scope["__inst_dict__"] = WeakValueDictionary()
+ if new_init := scope.get("__init__"):
+ # derive a new metaclass on the fly so we can ensure the
__call__
+ # signature carries the docs/annotations of the __init__ for
this class.
+ # Basically, show the __init__ doc/annotations, rather than
just the bare
+ # doc/annotations of cls.__call__
+ class c(cls):
+ __call__ = functools.wraps(new_init)(cls.__call__)
+ # wraps passes the original annotations; don't mutate the
underlying __init__
+ __call__.__annotations__ = new_init.__annotations__.copy()
+ __call__.__annotations__["disable_inst_caching"] = bool
+
+ c.__name__ = f"_{cls.__name__}_{name}"
+ cls = c
+
+ # if slots is in use, no __weakref__ slot disables weakref; inject it
if needed.
+ if (slots := scope.get("__slots__")) is not None:
+ if not any(hasattr(base, "__weakref__") for base in bases):
+ scope["__slots__"] = tuple(slots) + ("__weakref__",)
+
+ return type.__new__(cls, name, bases, scope)
def __call__(cls, *a, **kw):
"""disable caching via disable_inst_caching=True"""
- if cls.__inst_caching__ and not kw.pop("disable_inst_caching", False):
- kwlist = list(kw.items())
- kwlist.sort()
- key = (a, tuple(kwlist))
- try:
- instance = cls.__inst_dict__.get(key)
- except (NotImplementedError, TypeError) as t:
- warnings.warn(f"caching keys for {cls}, got {t} for a={a},
kw={kw}")
- del t
- key = instance = None
-
- if instance is None:
- instance = super(WeakInstMeta, cls).__call__(*a, **kw)
-
- if key is not None:
- cls.__inst_dict__[key] = instance
- else:
- instance = super(WeakInstMeta, cls).__call__(*a, **kw)
-
- return instance
+ # This is subtle, but note that this explictly passes
"disable_inst_caching" down to the class
+ # if the class itself has disabled caching. This is a debatable
design- it means any
+ # consumer that disables caching across a semver will throw an
exception here. However,
+ # this is historical behavior, thus left this way.
+ if not cls.__inst_caching__ or kw.pop("disable_inst_caching", False):
# type: ignore[attr-defined]
+ return super(WeakInstMeta, cls).__call__(*a, **kw)
+
+ try:
+ key = (a, tuple(sorted(kw.items())))
+ if None is (instance := cls.__inst_dict__.get(key)): # type:
ignore[attr-defined]
+ instance = cls.__inst_dict__[key] = super(WeakInstMeta,
cls).__call__(
+ *a, **kw
+ ) # type: ignore[attr-defined]
+ return instance
+ except (NotImplementedError, TypeError) as t:
+ warnings.warn(f"caching keys for {cls}, got {t} for a={a},
kw={kw}")
+ return super(WeakInstMeta, cls).__call__(*a, **kw)
diff --git a/tests/test_caching.py b/tests/test_caching.py
index 06615d3..5524c8b 100644
--- a/tests/test_caching.py
+++ b/tests/test_caching.py
@@ -169,3 +169,23 @@ class TestWeakInstMeta:
gc.collect()
o = weak_inst(unique)
assert weak_inst.counter == 2
+
+ def test_function_metadata(self):
+ def f(
+ self, x: str, y: int = 1, *a: tuple[str, ...], **kw: int
+ ) -> dict[str, int]:
+ "blah blah blah blah"
+ return {}
+
+ class c(metaclass=WeakInstMeta):
+ __inst_caching__ = True
+ __init__ = f # type: ignore
+
+ new_call = c.__class__.__call__
+ # ensure it's not mutating the reference functools passes.
+ assert new_call.__annotations__ is not f.__annotations__
+ assert new_call.__annotations__["disable_inst_caching"] == bool
+ a = new_call.__annotations__.copy()
+ a.pop("disable_inst_caching")
+ assert a == f.__annotations__
+ assert c.__init__.__doc__ == f.__doc__