Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 21 additions & 3 deletions redisdol/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,10 @@ def __iter__(self):
yield from self._source.keys()

def __getitem__(self, k):
return self._source.get(k)
v = self._source.get(k)
if v is None:
raise KeyError(k)
return v

def __contains__(self, k):
return k in self._source
Expand All @@ -90,7 +93,10 @@ def __len__(self):

class RedisBytesReader(RedisBytesCollection, KvReader):
def __getitem__(self, k):
return self._source.get(k)
v = self._source.get(k)
if v is None: # redis GET answers nil for a missing key; Mapping says KeyError
raise KeyError(k)
return v


class RedisBytesPersister(RedisBytesReader, KvPersister):
Expand Down Expand Up @@ -298,10 +304,22 @@ def _mk_list(self, k):

def __getitem__(self, k):
val_type = self._source.type(k)
if isinstance(val_type, str): # connection made with decode_responses=True
val_type = val_type.encode()
if val_type == b"string":
return self._source.get(k)
v = self._source.get(k)
if v is None: # expired/deleted between TYPE and GET
raise KeyError(k)
return v
elif val_type == b"list":
return self._mk_list(k)
elif val_type == b"none":
raise KeyError(k)
else: # hash, set, zset, stream...: present but not representable here
raise TypeError(
f"Key {k!r} holds a Redis {val_type.decode()!r}, "
"which this store can't read (only strings and lists)"
)


class RedisPersister(RedisCollection, RedisBytesPersister):
Expand Down
9 changes: 8 additions & 1 deletion redisdol/stores.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
def _convert_numeric_list(data, output_type=list):
if isinstance(data, RedisList):
return output_type(map(cast_to_numeric_if_possible, data))
return data # not a list (e.g. a string value): pass through untouched


@wrap_kvs(obj_of_data=_convert_numeric_list)
Expand All @@ -19,5 +20,11 @@ class RedisStoreWithNumericLists(RedisPersister):
>>> s['__test_numeric_list'] = [1,-2,-3.0, 4e-23, 'not_a_number']
>>> s['__test_numeric_list']
[1, -2, -3.0, 4e-23, b'not_a_number']
>>> del s['__test_numeric_list']
>>> s['__test_str'] = 'hello'
>>> s['__test_str'] # non-list values pass through (they used to come back as None)
b'hello'
>>> del s['__test_numeric_list'], s['__test_str']

Note that a list comes back as a detached python ``list`` (a snapshot): mutating
it does not write through to Redis, unlike the `RedisList` handle of `RedisPersister`.
"""
64 changes: 64 additions & 0 deletions tests/test_wrapping_fakeredis.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,3 +108,67 @@ def test_value_wrap_over_store_still_works_for_strings(monkeypatch):
w = wrap_kvs(RedisPersister, obj_of_data=lambda d: d)()
w["s"] = "v"
assert w["s"] == b"v"


# -- Mapping contract: missing keys raise KeyError (redisdol#4) ---------------------


@pytest.mark.parametrize("cls", [RedisBytesPersister, RedisPersister])
def test_missing_key_raises_keyerror(cls):
s = _store(cls)
with pytest.raises(KeyError):
s["absent"]
assert s.get("absent", "DEFAULT") == "DEFAULT"
assert "absent" not in s
s["present"] = "v"
assert s.get("present") == b"v"


def test_reader_missing_key_raises_keyerror():
from redisdol.base import RedisBytesReader

r = store_from_source(FakeRedis(), RedisBytesReader)
with pytest.raises(KeyError):
r["absent"]
assert r.get("absent", 0) == 0


def test_dict_of_store_roundtrips():
s = _store()
s["a"] = "x"
s["b"] = ["1", "2"]
d = dict(s)
assert d[b"a"] == b"x" and list(d[b"b"]) == [b"1", b"2"]


# -- RedisStoreWithNumericLists passes non-lists through (redisdol#5) ----------------


def test_numeric_lists_store_passes_strings_through(monkeypatch):
from redisdol.stores import RedisStoreWithNumericLists, _convert_numeric_list

monkeypatch.setattr("redisdol.base.Redis", FakeRedis)
s = RedisStoreWithNumericLists()
s["str"] = "hello"
s["nums"] = [1, -2, 3.5, "nope"]
assert s["str"] == b"hello"
assert s["nums"] == [1, -2, 3.5, b"nope"]
assert dict(s) == {b"str": b"hello", b"nums": [1, -2, 3.5, b"nope"]}
assert _convert_numeric_list("plain") == "plain"


def test_decode_responses_connection_missing_and_present_keys():
s = store_from_source(FakeRedis(decode_responses=True), RedisPersister)
s["k"] = "v"
s["l"] = ["a"]
assert s["k"] == "v" and list(s["l"]) == ["a"]
with pytest.raises(KeyError):
s["absent"]


def test_unsupported_redis_type_raises_typeerror_not_none():
s = _store()
s._source.hset("h", "f", "v")
assert "h" in s
with pytest.raises(TypeError, match="hash"):
s["h"]
Loading