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)

Reply via email to