commit: 075f2b0ad72464df613f2c74fd45e2f389e94fb7
Author: Brian Harring <ferringb <AT> gmail <DOT> com>
AuthorDate: Thu Nov 27 02:23:38 2025 +0000
Commit: Brian Harring <ferringb <AT> gmail <DOT> com>
CommitDate: Thu Nov 27 16:14:04 2025 +0000
URL:
https://gitweb.gentoo.org/proj/pkgcore/snakeoil.git/commit/?id=075f2b0a
feat: add get_subclasses_of
This is to replace KlassWalker, and functionality that is adhoc
doing walks of things like this. pkgcheck.objects should be
rebased to this, as will the slot shadowing logic.
Signed-off-by: Brian Harring <ferringb <AT> gmail.com>
src/snakeoil/klass/__init__.py | 14 ++++++++++---
src/snakeoil/klass/util.py | 32 ++++++++++++++++++++++++++++
tests/klass/test_util.py | 47 ++++++++++++++++++++++++++++++++++++++++++
3 files changed, 90 insertions(+), 3 deletions(-)
diff --git a/src/snakeoil/klass/__init__.py b/src/snakeoil/klass/__init__.py
index 36e38aa..15102e2 100644
--- a/src/snakeoil/klass/__init__.py
+++ b/src/snakeoil/klass/__init__.py
@@ -32,9 +32,10 @@ __all__ = (
"SlotsPicklingMixin",
"DirProxy",
"GetAttrProxy",
- "get_slots_of",
- "get_slot_of",
"get_attrs_of",
+ "get_slot_of",
+ "get_slots_of",
+ "get_subclasses_of",
)
import abc
@@ -64,7 +65,14 @@ from .properties import (
jit_attr_named,
jit_attr_none,
)
-from .util import combine_classes, copy_docs, get_attrs_of, get_slot_of,
get_slots_of
+from .util import (
+ combine_classes,
+ copy_docs,
+ get_attrs_of,
+ get_slot_of,
+ get_slots_of,
+ get_subclasses_of,
+)
sentinel = object()
diff --git a/src/snakeoil/klass/util.py b/src/snakeoil/klass/util.py
index 981e6c8..aee1b1c 100644
--- a/src/snakeoil/klass/util.py
+++ b/src/snakeoil/klass/util.py
@@ -2,6 +2,7 @@ __all__ = (
"get_attrs_of",
"get_slot_of",
"get_slots_of",
+ "get_subclasses_of",
"combine_classes",
"copy_class_docs",
"copy_docs",
@@ -10,6 +11,7 @@ __all__ = (
import builtins
import functools
+import inspect
import types
import typing
from typing import Any, Iterable
@@ -85,6 +87,36 @@ def get_attrs_of(
seen.add(slot)
+def get_subclasses_of(
+ cls: type,
+ only_leaf_nodes=False,
+ ABC: None | bool = None,
+) -> typing.Iterable[type]:
+ """yield the subclasses of the given class.
+
+ This walks the in memory tree of a class hierarchy, yield the subclasses
of the given
+ cls after optional filtering.
+
+ :param only_leaf_nodes: if True, only yield classes which have no
subclasses
+ :param ABC: if True, only yield abstract classes. If False, only yield
classes no longer
+ abstract. If None- the default- do no filtering for ABC.
+ """
+ stack = cls.__subclasses__()
+ while stack:
+ current = stack.pop()
+ subclasses = current.__subclasses__()
+ stack.extend(subclasses)
+
+ if ABC is not None:
+ if inspect.isabstract(current) != ABC:
+ continue
+
+ if not only_leaf_nodes:
+ yield current
+ elif not subclasses: # it's a leaf
+ yield current
+
+
@functools.lru_cache
def combine_classes(kls: type, *extra: type) -> type:
"""Given a set of classes, combine this as if one had wrote the class by
hand
diff --git a/tests/klass/test_util.py b/tests/klass/test_util.py
index 5736223..e397410 100644
--- a/tests/klass/test_util.py
+++ b/tests/klass/test_util.py
@@ -1,3 +1,5 @@
+import abc
+import operator
import weakref
from typing import Any
@@ -8,6 +10,7 @@ from snakeoil.klass.util import (
combine_classes,
get_attrs_of,
get_slots_of,
+ get_subclasses_of,
)
@@ -123,3 +126,47 @@ def test_combine_classes():
combined = combine_classes(kls1, kls2)
assert [combined, kls1, kls2, type, object] == list(combined.__mro__)
+
+
+def test_get_subclasses_of():
+ attr = operator.attrgetter("__name__")
+
+ def assert_it(cls, expected, msg=None, **kwargs):
+ expected = list(sorted(expected, key=attr))
+ got = list(sorted(get_subclasses_of(cls, **kwargs), key=attr))
+ if msg:
+ assert expected == got, msg
+ else:
+ assert expected == got
+
+ class layer1: ...
+
+ class layer2(layer1): ...
+
+ class layer3(layer2): ...
+
+ assert_it(layer3, [])
+ assert_it(layer2, [layer3])
+ assert_it(layer1, [layer2, layer3])
+ assert_it(layer1, [layer2, layer3], ABC=False)
+ assert_it(layer1, [], ABC=True)
+ assert_it(layer1, [layer3], only_leaf_nodes=True)
+
+ class ABClayer4(abc.ABC, layer3):
+ @abc.abstractmethod
+ def f(self): ...
+
+ class layer5(ABClayer4):
+ def f(self):
+ pass
+
+ assert_it(layer2, [layer3, layer5], ABC=False)
+ assert_it(layer2, [ABClayer4], ABC=True)
+ assert_it(layer2, [], ABC=True, only_leaf_nodes=True)
+ assert_it(layer2, [layer5], ABC=False, only_leaf_nodes=True)
+
+ class ABClayer6(layer5):
+ @abc.abstractmethod
+ def f2(self): ...
+
+ assert_it(layer3, [ABClayer6], ABC=True, only_leaf_nodes=True)