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]