From d9f55bcd829e886bc1d9ec789b9db16d672c0287 Mon Sep 17 00:00:00 2001 From: Thor Whalen <1906276+thorwhalen@users.noreply.github.com> Date: Tue, 13 Oct 2026 14:10:09 +0000 Subject: [PATCH 1/2] fix: store_from_source builds dol-wrapped classes (#10) Co-Authored-By: Claude Sonnet 5.5 --- redisdol/base.py | 40 +++++++++++++++++++++++++++++--- tests/test_wrapping_fakeredis.py | 36 ++++++++++++++++++++++++++++ 2 files changed, 73 insertions(+), 3 deletions(-) diff --git a/redisdol/base.py b/redisdol/base.py index f59c288..43e445b 100644 --- a/redisdol/base.py +++ b/redisdol/base.py @@ -4,7 +4,7 @@ import uuid from functools import wraps from collections.abc import Iterable -from dol.base import Collection, KvReader, KvPersister +from dol.base import Collection, KvReader, KvPersister, Store from redis import Redis from redis.exceptions import ResponseError @@ -45,7 +45,39 @@ def from_sourced_object(cls, obj): return cls.from_source(obj._source) +def _wrapped_class_of(cls): + """The class a ``dol`` class-wrapper (``wrap_kvs``, ``KeyCodecs...``, ``filt_iter``) + delegates to, read from the wrapper's ``__init__`` closure (``None`` if it has none).""" + init = getattr(cls, "__init__", None) + code, cells = getattr(init, "__code__", None), getattr(init, "__closure__", None) + if code is None or not cells: + return None + free = dict(zip(code.co_freevars, cells)) + if "wrapped" not in free or "delegation_attr" not in free: + return None + try: + wrapped, attr = free["wrapped"].cell_contents, free["delegation_attr"].cell_contents + except ValueError: # empty cell + return None + return (wrapped, attr) if isinstance(wrapped, type) else None + + def _new_with_source(cls, source): + if issubclass(cls, Store): + # A dol-wrapped class: its instances delegate to an instance of the wrapped + # class, so build that one over ``source`` and wrap it (i2mint/redisdol#10). + found = _wrapped_class_of(cls) + if found is None: + raise TypeError( + f"Can't build {cls.__name__} over an existing connection: it is a dol " + "Store whose wrapped class can't be determined. Build the store over " + "the connection first and wrap that instance instead." + ) + wrapped, attr = found + self = object.__new__(cls) + Store.__init__(self, _new_with_source(wrapped, source)) + assert attr == "store" + return self self = object.__new__(cls) self._source = source return self @@ -54,8 +86,10 @@ def _new_with_source(cls, source): def store_from_source(source, cls=None): """Make a store of type ``cls`` (default `RedisPersister`) over a ``Redis`` connection. - The returned store is a plain, *unwrapped* ``cls`` instance: it sees the whole - keyspace of ``source``. Apply any key/value codecs to the result yourself. + The returned store is a plain ``cls`` instance: it sees the whole keyspace of + ``source``. Apply any key/value codecs to the result yourself. ``cls`` may be a class + already wrapped with ``dol`` (e.g. `RedisStoreWithNumericLists`): its wrapped class is + built over ``source`` and wrapped as usual. >>> from fakeredis import FakeRedis >>> s = store_from_source(FakeRedis()) diff --git a/tests/test_wrapping_fakeredis.py b/tests/test_wrapping_fakeredis.py index 45cc89f..bfb7cba 100644 --- a/tests/test_wrapping_fakeredis.py +++ b/tests/test_wrapping_fakeredis.py @@ -318,3 +318,39 @@ def test_redislist_non_integer_index_is_typeerror(monkeypatch): l.insert(bad, "v") monkeypatch.setattr(RedisList, "iter_batch_size", 0) # degenerate: still terminates assert _dec(l) == ["a", "b"] + + +# ---- store_from_source on dol-wrapped classes (i2mint/redisdol#10) ---------------------- + + +def test_store_from_source_builds_wrapped_classes(): + from redisdol.stores import RedisStoreWithNumericLists + + r = FakeRedis() + s = store_from_source(r, RedisStoreWithNumericLists) + s["nums"] = [1, 2.5, "x"] + assert s["nums"] == [1, 2.5, b"x"] # the wrapper's codec is applied + s["str"] = "hello" + assert s["str"] == b"hello" + assert isinstance(s, RedisStoreWithNumericLists) + assert r.lrange("nums", 0, -1) == [b"1", b"2.5", b"x"] # over the given connection + + +def test_store_from_source_builds_stacked_wrappers(): + from redisdol.stores import RedisStoreWithNumericLists + + r = FakeRedis() + cls = KeyCodecs.prefixed("p:")(RedisStoreWithNumericLists) + s = store_from_source(r, cls) + s["k"] = [1, 2] + assert r.lrange("p:k", 0, -1) == [b"1", b"2"] and s["k"] == [1, 2] + + +def test_store_from_source_refuses_wrapped_class_it_cannot_unwrap(): + from dol import Store + + class Opaque(Store): # a Store subclass that is not a dol class-wrapper + pass + + with pytest.raises(TypeError, match="can't be determined"): + store_from_source(FakeRedis(), Opaque) From dac86363771b230e68c1355160af229d7bfd1473 Mon Sep 17 00:00:00 2001 From: Thor Whalen <1906276+thorwhalen@users.noreply.github.com> Date: Tue, 13 Oct 2026 14:13:13 +0000 Subject: [PATCH 2/2] fix: store_from_source runs the wrapper class's own __init__ (review of #13) Co-Authored-By: Claude Sonnet 5.5 --- redisdol/base.py | 23 +++++++++++++++-------- tests/test_wrapping_fakeredis.py | 17 +++++++++++++++++ 2 files changed, 32 insertions(+), 8 deletions(-) diff --git a/redisdol/base.py b/redisdol/base.py index 43e445b..430b580 100644 --- a/redisdol/base.py +++ b/redisdol/base.py @@ -46,20 +46,26 @@ def from_sourced_object(cls, obj): def _wrapped_class_of(cls): - """The class a ``dol`` class-wrapper (``wrap_kvs``, ``KeyCodecs...``, ``filt_iter``) - delegates to, read from the wrapper's ``__init__`` closure (``None`` if it has none).""" + """``(wrapped class, wrapper class)`` of a ``dol`` class-wrapper (``wrap_kvs``, + ``KeyCodecs...``, ``filt_iter``), read from the closure of the ``__init__`` dol + generates for it (``None`` if it has none, or delegates through another attribute + than ``store``).""" init = getattr(cls, "__init__", None) code, cells = getattr(init, "__code__", None), getattr(init, "__closure__", None) if code is None or not cells: return None free = dict(zip(code.co_freevars, cells)) - if "wrapped" not in free or "delegation_attr" not in free: + if not {"wrapped", "delegation_attr", "__class__"} <= free.keys(): return None try: - wrapped, attr = free["wrapped"].cell_contents, free["delegation_attr"].cell_contents + wrapped = free["wrapped"].cell_contents + attr = free["delegation_attr"].cell_contents + wrapper = free["__class__"].cell_contents except ValueError: # empty cell return None - return (wrapped, attr) if isinstance(wrapped, type) else None + if not (isinstance(wrapped, type) and isinstance(wrapper, type) and attr == "store"): + return None + return wrapped, wrapper def _new_with_source(cls, source): @@ -73,10 +79,11 @@ def _new_with_source(cls, source): "Store whose wrapped class can't be determined. Build the store over " "the connection first and wrap that instance instead." ) - wrapped, attr = found + wrapped, wrapper = found self = object.__new__(cls) - Store.__init__(self, _new_with_source(wrapped, source)) - assert attr == "store" + # what dol's generated __init__ does after building the delegate: the wrapper + # class's own __init__ (which may set state of its own), not Store's directly + super(wrapper, self).__init__(_new_with_source(wrapped, source)) return self self = object.__new__(cls) self._source = source diff --git a/tests/test_wrapping_fakeredis.py b/tests/test_wrapping_fakeredis.py index bfb7cba..3d2af0b 100644 --- a/tests/test_wrapping_fakeredis.py +++ b/tests/test_wrapping_fakeredis.py @@ -354,3 +354,20 @@ class Opaque(Store): # a Store subclass that is not a dol class-wrapper with pytest.raises(TypeError, match="can't be determined"): store_from_source(FakeRedis(), Opaque) + + +def test_store_from_source_runs_the_wrapper_classes_own_init(): + from dol import Store + + class Tagged(Store): + def __init__(self, store, tag="T"): + super().__init__(store) + self.tag = tag + + cls = Tagged.wrap(RedisPersister) + s = store_from_source(FakeRedis(), cls) + assert s.tag == "T" + s["k"] = "v" + assert s["k"] == b"v" + cls2 = wrap_kvs(RedisPersister, wrapper=Tagged, obj_of_data=lambda v: v) + assert store_from_source(FakeRedis(), cls2).tag == "T"