commit: a836473218ee0e41cc989ee07db7a9bb6f619c4a
Author: Brian Harring <ferringb <AT> gmail <DOT> com>
AuthorDate: Mon Oct 27 16:15:27 2025 +0000
Commit: Arthur Zamarin <arthurzam <AT> gentoo <DOT> org>
CommitDate: Mon Oct 27 21:54:08 2025 +0000
URL:
https://gitweb.gentoo.org/proj/pkgcore/snakeoil.git/commit/?id=a8364732
chore: mark steal_docs as deprecated in favor of wraps() or copy_docs()
The steal_docs api signature was fairly daft and py3k tooling allows doing
this saner, so do so. copy_docs can handle both class and function, removing
a ton of noise.
In parallel to cleaning that out, convert to functools.wraps where appropriate,
and drop docstrings where py3k now transfers annotations and docs across.
Also do the full deprecation cleanup for this change also.
Signed-off-by: Brian Harring <ferringb <AT> gmail.com>
src/snakeoil/containers.py | 30 +++++---------------
src/snakeoil/data_source.py | 12 ++------
src/snakeoil/formatters.py | 14 ++++-----
src/snakeoil/klass/__init__.py | 54 ++---------------------------------
src/snakeoil/klass/deprecated.py | 51 +++++++++++++++++++++++++++++++++
src/snakeoil/klass/util.py | 61 +++++++++++++++++++++++++++++++++++++++-
src/snakeoil/mappings.py | 27 ++++++------------
src/snakeoil/sequences.py | 6 ++--
8 files changed, 140 insertions(+), 115 deletions(-)
diff --git a/src/snakeoil/containers.py b/src/snakeoil/containers.py
index ee14baf..0660207 100644
--- a/src/snakeoil/containers.py
+++ b/src/snakeoil/containers.py
@@ -14,7 +14,7 @@ __all__ = (
from itertools import chain, filterfalse
-from .klass import steal_docs
+from .klass import copy_docs
class InvertedContains(set):
@@ -41,6 +41,7 @@ class InvertedContains(set):
raise TypeError("InvertedContains cannot be iterated over")
+@copy_docs(set)
class SetMixin:
"""
Base class for implementing set classes
@@ -52,26 +53,21 @@ class SetMixin:
"""
- @steal_docs(set)
def __and__(self, other, kls=None):
# Note: for these methods we don't bother to filter dupes from this
# list - since the subclasses __init__ should already handle this,
# there's no point doing it twice.
return (kls or self.__class__)(x for x in self if x in other)
- @steal_docs(set)
def __rand__(self, other):
return self.__and__(other, kls=other.__class__)
- @steal_docs(set)
def __or__(self, other, kls=None):
return (kls or self.__class__)(chain(self, other))
- @steal_docs(set)
def __ror__(self, other):
return self.__or__(other, kls=other.__class__)
- @steal_docs(set)
def __xor__(self, other, kls=None):
return (kls or self.__class__)(
chain(
@@ -79,20 +75,17 @@ class SetMixin:
)
)
- @steal_docs(set)
def __rxor__(self, other):
return self.__xor__(other, kls=other.__class__)
- @steal_docs(set)
def __sub__(self, other):
return self.__class__(x for x in self if x not in other)
- @steal_docs(set)
def __rsub__(self, other):
return other.__class__(x for x in other if x not in self)
- __add__ = steal_docs(set)(__or__)
- __radd__ = steal_docs(set)(__ror__)
+ __add__ = __or__
+ __radd__ = __ror__
class LimitedChangeSet(SetMixin):
@@ -147,7 +140,6 @@ class LimitedChangeSet(SetMixin):
self._change_order = []
self._orig = frozenset(self._new)
- @steal_docs(set)
def add(self, key):
key = self._validater(key)
if key in self._changed or key in self._blacklist:
@@ -160,7 +152,6 @@ class LimitedChangeSet(SetMixin):
self._changed.add(key)
self._change_order.append((self._added, key))
- @steal_docs(set)
def remove(self, key):
key = self._validater(key)
if key in self._changed or key in self._blacklist:
@@ -173,7 +164,6 @@ class LimitedChangeSet(SetMixin):
self._changed.add(key)
self._change_order.append((self._removed, key))
- @steal_docs(set)
def __contains__(self, key):
return self._validater(key) in self._new
@@ -201,15 +191,12 @@ class LimitedChangeSet(SetMixin):
def __str__(self):
return "LimitedChangeSet([%s])" % (str(self._new)[1:-1],)
- @steal_docs(set)
def __iter__(self):
return iter(self._new)
- @steal_docs(set)
def __len__(self):
return len(self._new)
- @steal_docs(set)
def __eq__(self, other):
if isinstance(other, LimitedChangeSet):
return self._new == other._new
@@ -217,7 +204,6 @@ class LimitedChangeSet(SetMixin):
return self._new == other
return False
- @steal_docs(set)
def __ne__(self, other):
return not self == other
@@ -260,6 +246,7 @@ class ProtectedSet(SetMixin):
self._new.add(key)
+@copy_docs(set)
class RefCountingSet(dict):
"""
Set implementation that implements refcounting for add/remove, removing
the key only when its refcount is 0.
@@ -281,12 +268,10 @@ class RefCountingSet(dict):
if iterable is not None:
self.update(iterable)
- @steal_docs(set)
def add(self, item):
count = self.get(item, 0)
self[item] = count + 1
- @steal_docs(set)
def remove(self, item):
count = self[item]
if count == 1:
@@ -294,14 +279,13 @@ class RefCountingSet(dict):
else:
self[item] = count - 1
- @steal_docs(set)
def discard(self, item):
try:
self.remove(item)
except KeyError:
pass
- @steal_docs(set)
- def update(self, items):
+ # the signature conflict is expected, thus the incompatible override.
+ def update(self, items): # pyright:
ignore[reportIncompatibleMethodOverride]
for item in items:
self.add(item)
diff --git a/src/snakeoil/data_source.py b/src/snakeoil/data_source.py
index bb265d9..7c9e4ea 100644
--- a/src/snakeoil/data_source.py
+++ b/src/snakeoil/data_source.py
@@ -43,10 +43,10 @@ __all__ = (
)
import errno
-from functools import partial
import io
+from functools import partial
-from . import compression, fileutils, klass, stringio
+from . import compression, fileutils, stringio
from .currying import post_curry
@@ -210,7 +210,6 @@ class local_source(base):
self.mutable = mutable
self.encoding = encoding
- @klass.steal_docs(base)
def text_fileobj(self, writable=False):
if writable and not self.mutable:
raise TypeError("data source %s is immutable" % (self,))
@@ -230,7 +229,6 @@ class local_source(base):
raise
return opener(self.path, "w+")
- @klass.steal_docs(base)
def bytes_fileobj(self, writable=False):
if not writable:
return open_file(self.path, "rb", self.buffering_window)
@@ -321,7 +319,6 @@ class data_source(base):
return self.data
return self.data.decode()
- @klass.steal_docs(base)
def text_fileobj(self, writable=False):
if writable:
if not self.mutable:
@@ -337,7 +334,6 @@ class data_source(base):
data = data.decode()
self.data = data
- @klass.steal_docs(base)
def bytes_fileobj(self, writable=False):
if writable:
if not self.mutable:
@@ -354,7 +350,6 @@ class text_data_source(data_source):
__slots__ = ()
- @klass.steal_docs(data_source)
def __init__(self, data, mutable=False):
if not isinstance(data, str):
raise TypeError("data must be a str")
@@ -374,7 +369,6 @@ class bytes_data_source(data_source):
__slots__ = ()
- @klass.steal_docs(data_source)
def __init__(self, data, mutable=False):
if not isinstance(data, bytes):
raise TypeError("data must be bytes")
@@ -405,13 +399,11 @@ class invokable_data_source(data_source):
"""
data_source.__init__(self, data, mutable=False)
- @klass.steal_docs(data_source)
def text_fileobj(self, writable=False):
if writable:
raise TypeError(f"data source {self} data is immutable")
return self.data(True)
- @klass.steal_docs(data_source)
def bytes_fileobj(self, writable=False):
if writable:
raise TypeError(f"data source {self} data is immutable")
diff --git a/src/snakeoil/formatters.py b/src/snakeoil/formatters.py
index 505e137..ee1840f 100644
--- a/src/snakeoil/formatters.py
+++ b/src/snakeoil/formatters.py
@@ -7,7 +7,7 @@ import os
import typing
from functools import partial
-from .klass import GetAttrProxy, steal_docs
+from .klass import GetAttrProxy, copy_docs
from .mappings import defaultdictkey
__all__ = (
@@ -201,7 +201,6 @@ class PlainTextFormatter(Formatter):
# but it is completely arbitrary.
self._pos = self.width - 10
- @steal_docs(Formatter)
def write(self, *args, **kwargs):
wrap = kwargs.get("wrap", self.wrap)
autoline = kwargs.get("autoline", self.autoline)
@@ -487,15 +486,14 @@ else:
self._fg_cache = defaultdictkey(partial(TerminfoColor, 0))
self._bg_cache = defaultdictkey(partial(TerminfoColor, 1))
- @steal_docs(Formatter)
+ @copy_docs(Formatter.fg)
def fg(self, color=None):
return self._fg_cache[color]
- @steal_docs(Formatter)
+ @copy_docs(Formatter.bg)
def bg(self, color=None):
return self._bg_cache[color]
- @steal_docs(Formatter)
def write(self, *args, **kwargs):
super().write(*args, **kwargs)
try:
@@ -509,8 +507,8 @@ else:
raise StreamClosed(e)
raise
- @steal_docs(Formatter)
- def title(self, string):
+ def title(self, title: str): # pyright:
ignore[reportIncompatibleMethodOverride]
+ """ "Set the title"""
# I want to use curses.tigetflag('hs') here but at least
# the screen-s entry defines a tsl and fsl string but does
# not set the hs flag. So just check for the ability to
@@ -519,7 +517,7 @@ else:
tsl = curses.tigetstr("tsl")
fsl = curses.tigetstr("fsl")
if tsl and fsl:
- self.stream.write(tsl + string.encode(self.encoding,
"replace") + fsl)
+ self.stream.write(tsl + title.encode(self.encoding, "replace")
+ fsl)
self.stream.flush()
diff --git a/src/snakeoil/klass/__init__.py b/src/snakeoil/klass/__init__.py
index 53abce0..7fa587b 100644
--- a/src/snakeoil/klass/__init__.py
+++ b/src/snakeoil/klass/__init__.py
@@ -21,6 +21,7 @@ __all__ = (
"cached_hash",
"cached_property",
"cached_property_named",
+ "copy_docs",
"steal_docs",
"ImmutableInstance",
"immutable_instance",
@@ -36,7 +37,6 @@ __all__ = (
)
import abc
-import inspect
from collections import deque
from operator import attrgetter
@@ -48,6 +48,7 @@ from .deprecated import (
immutable_instance,
inject_immutable_instance,
inject_richcmp_methods_from_cmp,
+ steal_docs,
)
from .properties import (
_uncached_singleton, # noqa: F401 . This exists purely due to a stupid
usage of pkgcore.ebuild.profile which is being removed.
@@ -62,7 +63,7 @@ from .properties import (
jit_attr_named,
jit_attr_none,
)
-from .util import combine_classes, get_attrs_of, get_slots_of
+from .util import combine_classes, copy_docs, get_attrs_of, get_slots_of
sentinel = object()
@@ -407,55 +408,6 @@ def cached_hash(func):
return __hash__
-def steal_docs(target, ignore_missing=False, name=None):
- """
- decorator to steal __doc__ off of a target class or function
-
- Specifically when the target is a class, it will look for a member matching
- the functors names from target, and clones those docs to that functor;
- otherwise, it will simply clone the targeted function's docs to the
- functor.
-
- :param target: class or function to steal docs from
- :param ignore_missing: if True, it'll swallow the exception if it
- cannot find a matching method on the target_class. This is rarely
- what you want- it's mainly useful for cases like `dict.has_key`, where
it
- exists in py2k but doesn't in py3k
- :param name: function name from class to steal docs from, by default the
name of the
- decorated function is used; only used when the target is a class name
-
- Example Usage:
-
- >>> from snakeoil.klass import steal_docs
- >>> class foo(list):
- ... @steal_docs(list)
- ... def extend(self, *a):
- ... pass
- >>>
- >>> f = foo([1,2,3])
- >>> assert f.extend.__doc__ == list.extend.__doc__
- """
-
- def inner(functor):
- if inspect.isclass(target):
- if name is not None:
- target_name = name
- else:
- target_name = functor.__name__
- try:
- obj = getattr(target, target_name)
- except AttributeError:
- if not ignore_missing:
- raise
- return functor
- else:
- obj = target
- functor.__doc__ = obj.__doc__
- return functor
-
- return inner
-
-
class SlotsPicklingMixin:
"""Default pickling support for classes that use __slots__."""
diff --git a/src/snakeoil/klass/deprecated.py b/src/snakeoil/klass/deprecated.py
index 9c9d5a1..79931c3 100644
--- a/src/snakeoil/klass/deprecated.py
+++ b/src/snakeoil/klass/deprecated.py
@@ -2,6 +2,7 @@
__all__ = ("immutable_instance", "inject_immutable_instance",
"ImmutableInstance")
+import inspect
import typing
from snakeoil.deprecation import deprecated, suppress_deprecation_warning
@@ -134,3 +135,53 @@ def inject_richcmp_methods_from_cmp(scope):
("__gt__", __generic_gt),
):
scope.setdefault(key, func)
+
+
+@deprecated("snakeoil.klass.steal_docs is deprecated; use functools.wraps")
+def steal_docs(target, ignore_missing=False, name=None):
+ """
+ decorator to steal __doc__ off of a target class or function
+
+ Specifically when the target is a class, it will look for a member
matching
+ the functors names from target, and clones those docs to that functor;
+ otherwise, it will simply clone the targeted function's docs to the
+ functor.
+
+ :param target: class or function to steal docs from
+ :param ignore_missing: if True, it'll swallow the exception if it
+ cannot find a matching method on the target_class. This is rarely
+ what you want- it's mainly useful for cases like `dict.has_key`,
where it
+ exists in py2k but doesn't in py3k
+ :param name: function name from class to steal docs from, by default
the name of the
+ decorated function is used; only used when the target is a class
name
+
+ Example Usage:
+
+ >>> from snakeoil.klass import steal_docs
+ >>> class foo(list):
+ ... @steal_docs(list)
+ ... def extend(self, *a):
+ ... pass
+ >>>
+ >>> f = foo([1,2,3])
+ >>> assert f.extend.__doc__ == list.extend.__doc__
+ """
+
+ def inner(functor):
+ if inspect.isclass(target):
+ if name is not None:
+ target_name = name
+ else:
+ target_name = functor.__name__
+ try:
+ obj = getattr(target, target_name)
+ except AttributeError:
+ if not ignore_missing:
+ raise
+ return functor
+ else:
+ obj = target
+ functor.__doc__ = obj.__doc__
+ return functor
+
+ return inner
diff --git a/src/snakeoil/klass/util.py b/src/snakeoil/klass/util.py
index 888c4a7..73d9bce 100644
--- a/src/snakeoil/klass/util.py
+++ b/src/snakeoil/klass/util.py
@@ -1,6 +1,13 @@
-__all__ = ("get_attrs_of", "get_slots_of", "combine_classes")
+__all__ = (
+ "get_attrs_of",
+ "get_slots_of",
+ "combine_classes",
+ "copy_class_docs",
+ "copy_docs",
+)
import builtins
import functools
+import types
from typing import Any, Iterable
_known_builtins = frozenset(
@@ -88,3 +95,55 @@ def combine_classes(kls: type, *extra: type) -> type:
combined.__name__ = f"combined_{'_'.join(kls.__qualname__ for kls in
klses)}"
return combined
+
+
+# For this list, look at functools.wraps for an idea of what is possibly
mappable.
+_copy_doc_targets = ("__annotations__", "__doc__", "__type_params__")
+
+
+def copy_docs(target):
+ """Copy the docs and annotations off of the given target
+
+ This is used for implementations that look like something (the target), but
+ do not actually invoke the the target.
+
+ If you're just wrapping something- a true decorator- use functools.wraps
+ """
+
+ if isinstance(target, type):
+ return copy_class_docs(target)
+
+ def inner(functor):
+ for name in _copy_doc_targets:
+ try:
+ setattr(functor, name, getattr(target, name))
+ except AttributeError:
+ pass
+ return functor
+
+ return inner
+
+
+def copy_class_docs(source_class):
+ """
+ Copy the docs and annotations of a target class for methods that intersect
with the target.
+
+ This does *not* check that the prototype signatures are the same, and it
exempts __init__
+ since that makes no sense to copy
+ """
+
+ def do_it(cls):
+ if cls.__name__ == "OrderedFrozenSet":
+ import pdb
+
+ pdb.set_trace()
+ for name in set(source_class.__dict__).intersection(cls.__dict__):
+ obj = getattr(cls, name)
+ if not isinstance(obj, types.FunctionType):
+ continue
+ if getattr(obj, "__annotations__", None) or getattr(obj,
"__doc__", None):
+ continue
+ setattr(cls, name, copy_docs(getattr(source_class, name))(obj))
+ return cls
+
+ return do_it
diff --git a/src/snakeoil/mappings.py b/src/snakeoil/mappings.py
index 657d4de..26dd76f 100644
--- a/src/snakeoil/mappings.py
+++ b/src/snakeoil/mappings.py
@@ -19,13 +19,14 @@ __all__ = (
import operator
from collections import defaultdict
from collections.abc import Mapping, MutableSet, Set
-from functools import partial
+from functools import partial, wraps
from itertools import chain, filterfalse, islice
from typing import Any
-from .klass import contains, get, get_attrs_of, sentinel, steal_docs
+from .klass import contains, copy_docs, get, get_attrs_of, sentinel
+@copy_docs(dict)
class DictMixin:
"""
new style class replacement for :py:func:`UserDict.DictMixin`
@@ -58,28 +59,22 @@ class DictMixin:
if kwargs:
self.update(kwargs.items())
- @steal_docs(dict)
def __iter__(self):
return self.keys()
- @steal_docs(dict)
def __str__(self):
return str(dict(self.items()))
- @steal_docs(dict)
def items(self):
for k in self:
yield k, self[k]
- @steal_docs(dict)
def keys(self):
raise NotImplementedError(self, "keys")
- @steal_docs(dict)
def values(self):
return map(self.__getitem__, self)
- @steal_docs(dict)
def update(self, iterable):
for k, v in iterable:
self[k] = v
@@ -87,7 +82,6 @@ class DictMixin:
get = get
__contains__ = contains
- @steal_docs(dict)
def __eq__(self, other):
if len(self) != len(other):
return False
@@ -98,11 +92,9 @@ class DictMixin:
return False
return True
- @steal_docs(dict)
def __ne__(self, other):
return not self.__eq__(other)
- @steal_docs(dict)
def pop(self, key, default=sentinel):
if not self.__externally_mutable__:
raise AttributeError(self, "pop")
@@ -115,7 +107,6 @@ class DictMixin:
raise
return val
- @steal_docs(dict)
def setdefault(self, key, default=None):
if not self.__externally_mutable__:
raise AttributeError(self, "setdefault")
@@ -137,7 +128,6 @@ class DictMixin:
raise AttributeError(self, "__delitem__")
raise NotImplementedError(self, "__delitem__")
- @steal_docs(dict)
def clear(self):
if not self.__externally_mutable__:
raise AttributeError(self, "clear")
@@ -159,7 +149,6 @@ class DictMixin:
return True
return False
- @steal_docs(dict)
def popitem(self):
if not self.__externally_mutable__:
raise AttributeError(self, "popitem")
@@ -445,6 +434,7 @@ class OrderedSet(OrderedFrozenSet, MutableSet):
raise TypeError(f"unhashable type: {self.__class__.__name__!r}")
+@copy_docs(dict)
class IndeterminantDict:
"""A wrapped dict with constant defaults, and a function for other keys.
@@ -528,7 +518,7 @@ class StackedDict(DictMixin):
def __setitem__(self, *a):
raise TypeError("unmodifiable")
- __delitem__ = clear = __setitem__
+ __delitem__ = clear = __setitem__ # pyright: ignore[reportAssignmentType]
class PreservingFoldingDict(DictMixin):
@@ -624,6 +614,7 @@ class NonPreservingFoldingDict(DictMixin):
def __setitem__(self, key, value):
self._dict[self._folder(key)] = value
+ return value
def __delitem__(self, key):
del self._dict[self._folder(key)]
@@ -661,21 +652,19 @@ class defaultdictkey(defaultdict):
# that a default_factory is required
defaultdict.__init__(self, default_factory)
- @steal_docs(defaultdict)
def __missing__(self, key):
obj = self[key] = self.default_factory(key)
return obj
def _KeyError_to_Attr(functor):
+ @wraps(functor)
def inner(self, *args):
try:
return functor(self, *args)
except KeyError:
raise AttributeError(args[0])
- inner.__name__ = functor.__name__
- inner.__doc__ = functor.__doc__
return inner
@@ -756,7 +745,7 @@ class ProxiedAttrs(DictMixin):
except AttributeError:
raise KeyError(key)
- def __delitem__(self, key: Any) -> None:
+ def __delitem__(self, key: Any):
try:
return delattr(self.__target__, key)
except AttributeError:
diff --git a/src/snakeoil/sequences.py b/src/snakeoil/sequences.py
index b7e0ba7..b075bfb 100644
--- a/src/snakeoil/sequences.py
+++ b/src/snakeoil/sequences.py
@@ -11,10 +11,10 @@ __all__ = (
"split_negations",
)
+import functools
from typing import Any, Callable, Iterable, Type
from .iterables import expandable_chain
-from .klass import steal_docs
def unstable_unique(sequence):
@@ -223,11 +223,11 @@ class ChainedLists:
def __str__(self):
return "[ %s ]" % ", ".join(str(l) for l in self._lists)
- @steal_docs(list)
+ @functools.wraps(list.append)
def append(self, item):
self._lists.append(item)
- @steal_docs(list)
+ @functools.wraps(list.extend)
def extend(self, items):
self._lists.extend(items)