commit:     edd673f31f5c631c3a25259b21900e2a22247299
Author:     Brian Harring <ferringb <AT> gmail <DOT> com>
AuthorDate: Sat Nov 29 15:48:03 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=edd673f3

feat(klass): add get_instances_of to find all instances in memory.

This is pretty much only for tests, in particular it'll be used
for doing demandload and delayed instantion validation.  We have
to find the instances to check them, thus the easiest way is to
just walk all objects in memory and dig them out.

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

 src/snakeoil/klass/util.py | 23 +++++++++++++++++++
 tests/klass/test_util.py   | 56 ++++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 79 insertions(+)

diff --git a/src/snakeoil/klass/util.py b/src/snakeoil/klass/util.py
index 4d6aca3..0cbdd72 100644
--- a/src/snakeoil/klass/util.py
+++ b/src/snakeoil/klass/util.py
@@ -1,5 +1,6 @@
 __all__ = (
     "get_attrs_of",
+    "get_instances_of",
     "get_slot_of",
     "get_slots_of",
     "get_subclasses_of",
@@ -12,6 +13,7 @@ __all__ = (
 
 import builtins
 import functools
+import gc
 import inspect
 import types
 import typing
@@ -138,6 +140,27 @@ def get_subclasses_of(
             yield current
 
 
+def get_instances_of(cls: type, getattribute=False) -> list[type]:
+    """
+    Find all instances of the class in memory that are reachable by python GC.
+
+    Certain cpython types may not implement visitation correctly- they're 
broke,
+    but they will hide the instances from this.  This should never happen, but
+    if you know an instance exists and is in cpython object, this is the 'why'
+
+    Note: this uses object.__getattribute__ directly.  Even if your instance 
has
+    a __getattribute__ that returns a __class__ that isn't it's actuall class, 
this
+    *will* find it.
+    """
+    return [
+        x
+        for x in gc.get_referrers(cls)
+        # __getattribute__ because certain thunking implementations also lie as
+        # what class they are- they proxy in a way they appear as their target.
+        if object.__getattribute__(x, "__class__") is cls
+    ]
+
+
 @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 81990be..3fc4008 100644
--- a/tests/klass/test_util.py
+++ b/tests/klass/test_util.py
@@ -1,4 +1,5 @@
 import abc
+import gc
 import operator
 import weakref
 from typing import Any
@@ -9,6 +10,7 @@ from snakeoil.klass.util import (
     ClassSlotting,
     combine_classes,
     get_attrs_of,
+    get_instances_of,
     get_slots_of,
     get_subclasses_of,
     is_metaclass,
@@ -191,3 +193,57 @@ def test_get_subclasses_of():
     class combined(left, right): ...
 
     assert_it(base, [left, right, combined])
+
+
+class Test_get_instances_of:
+    def test_normal(self):
+        class foon: ...
+
+        assert [] == get_instances_of(foon)
+        o = foon()
+        assert [o] == get_instances_of(foon)
+
+        o2 = foon()
+        assert set([o, o2]) == set(get_instances_of(foon))
+
+    def test_verify_sees_through_thunks(self):
+        """
+        Parts of snakeoil infrastructure provide thunking proxies that lie 
*very* well.
+
+        Specifically, if you ask them what their class is, they'll tell you 
they're the thing
+        that they'll eventually reify.  The testing infrastructure for 
demandload is reliant
+        on being able to see through this, thus this test asserts that 
get_instances_of can
+        see through that.
+        """
+
+        class foo: ...
+
+        class liar:
+            def __init__(self, real):
+                object.__setattr__(self, "_real", real)
+
+            def __getattribute__(self, attr):
+                return object.__getattribute__(self, "_real").__class__
+
+            def __eq__(self, other):
+                return object.__getattribute__(self, "_real") == other
+
+        real = foo()
+        # validate our setup.  Note the level of hiding it does.
+        # Demandload infrastructure actually emulates the full protocol of the 
target's
+        # cpython guts, so that hides *far* more thoroughly.  Only 
object.__getattribute__
+        # can see through that, and python doesn't use that; certain cpython 
parts use an
+        # equivalent, but if you can lie to isinstance... etc.
+        assert foo is liar(real).__class__
+        assert isinstance(liar(real), foo)
+        assert real == liar(real)
+
+        # can't lie about that one however since it's a pointer check
+        assert id(real) != id(liar)
+        assert real is not liar
+
+        assert [real] == get_instances_of(foo)
+        liar_obj = liar(real)
+        collected = get_instances_of(liar)
+        assert 1 == len(collected)
+        assert liar_obj is collected[0]

Reply via email to