Skip to content

Commit 8d3c615

Browse files
authored
Support for json multipath ($) (#1663)
1 parent 72b4926 commit 8d3c615

File tree

8 files changed

+1823
-74
lines changed

8 files changed

+1823
-74
lines changed

redis/commands/helpers.py

+4-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,10 @@ def list_or_args(keys, args):
1717

1818
def nativestr(x):
1919
"""Return the decoded binary string, or a string, depending on type."""
20-
return x.decode("utf-8", "replace") if isinstance(x, bytes) else x
20+
r = x.decode("utf-8", "replace") if isinstance(x, bytes) else x
21+
if r == 'null':
22+
return
23+
return r
2124

2225

2326
def delist(x):

redis/commands/json/__init__.py

+26-19
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
1-
from json import JSONDecoder, JSONEncoder
1+
from json import JSONDecoder, JSONEncoder, JSONDecodeError
22

33
from .decoders import (
4-
int_or_list,
5-
int_or_none
4+
decode_list,
5+
bulk_of_jsons,
66
)
7-
from .helpers import bulk_of_jsons
8-
from ..helpers import nativestr, delist
7+
from ..helpers import nativestr
98
from .commands import JSONCommands
109

1110

@@ -46,19 +45,19 @@ def __init__(
4645
"JSON.SET": lambda r: r and nativestr(r) == "OK",
4746
"JSON.NUMINCRBY": self._decode,
4847
"JSON.NUMMULTBY": self._decode,
49-
"JSON.TOGGLE": lambda b: b == b"true",
50-
"JSON.STRAPPEND": int,
51-
"JSON.STRLEN": int,
52-
"JSON.ARRAPPEND": int,
53-
"JSON.ARRINDEX": int,
54-
"JSON.ARRINSERT": int,
55-
"JSON.ARRLEN": int_or_none,
48+
"JSON.TOGGLE": self._decode,
49+
"JSON.STRAPPEND": self._decode,
50+
"JSON.STRLEN": self._decode,
51+
"JSON.ARRAPPEND": self._decode,
52+
"JSON.ARRINDEX": self._decode,
53+
"JSON.ARRINSERT": self._decode,
54+
"JSON.ARRLEN": self._decode,
5655
"JSON.ARRPOP": self._decode,
57-
"JSON.ARRTRIM": int,
58-
"JSON.OBJLEN": int,
59-
"JSON.OBJKEYS": delist,
60-
# "JSON.RESP": delist,
61-
"JSON.DEBUG": int_or_list,
56+
"JSON.ARRTRIM": self._decode,
57+
"JSON.OBJLEN": self._decode,
58+
"JSON.OBJKEYS": self._decode,
59+
"JSON.RESP": self._decode,
60+
"JSON.DEBUG": self._decode,
6261
}
6362

6463
self.client = client
@@ -77,9 +76,17 @@ def _decode(self, obj):
7776
return obj
7877

7978
try:
80-
return self.__decoder__.decode(obj)
79+
x = self.__decoder__.decode(obj)
80+
if x is None:
81+
raise TypeError
82+
return x
8183
except TypeError:
82-
return self.__decoder__.decode(obj.decode())
84+
try:
85+
return self.__decoder__.decode(obj.decode())
86+
except AttributeError:
87+
return decode_list(obj)
88+
except (AttributeError, JSONDecodeError):
89+
return decode_list(obj)
8390

8491
def _encode(self, obj):
8592
"""Get the encoder."""

redis/commands/json/commands.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from .path import Path
2-
from .helpers import decode_dict_keys
2+
from .decoders import decode_dict_keys
33
from deprecated import deprecated
44
from redis.exceptions import DataError
55

@@ -192,7 +192,7 @@ def strappend(self, name, value, path=Path.rootPath()):
192192
the key name, the path is determined to be the first. If a single
193193
option is passed, then the rootpath (i.e Path.rootPath()) is used.
194194
"""
195-
pieces = [name, str(path), value]
195+
pieces = [name, str(path), self._encode(value)]
196196
return self.execute_command(
197197
"JSON.STRAPPEND", *pieces
198198
)

redis/commands/json/decoders.py

+56-9
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,59 @@
1-
def int_or_list(b):
2-
if isinstance(b, int):
3-
return b
4-
else:
5-
return b
1+
from ..helpers import nativestr
2+
import re
3+
import copy
4+
65

6+
def bulk_of_jsons(d):
7+
"""Replace serialized JSON values with objects in a
8+
bulk array response (list).
9+
"""
710

8-
def int_or_none(b):
9-
if b is None:
10-
return None
11-
if isinstance(b, int):
11+
def _f(b):
12+
for index, item in enumerate(b):
13+
if item is not None:
14+
b[index] = d(item)
1215
return b
16+
17+
return _f
18+
19+
20+
def decode_dict_keys(obj):
21+
"""Decode the keys of the given dictionary with utf-8."""
22+
newobj = copy.copy(obj)
23+
for k in obj.keys():
24+
if isinstance(k, bytes):
25+
newobj[k.decode("utf-8")] = newobj[k]
26+
newobj.pop(k)
27+
return newobj
28+
29+
30+
def unstring(obj):
31+
"""
32+
Attempt to parse string to native integer formats.
33+
One can't simply call int/float in a try/catch because there is a
34+
semantic difference between (for example) 15.0 and 15.
35+
"""
36+
floatreg = '^\\d+.\\d+$'
37+
match = re.findall(floatreg, obj)
38+
if match != []:
39+
return float(match[0])
40+
41+
intreg = "^\\d+$"
42+
match = re.findall(intreg, obj)
43+
if match != []:
44+
return int(match[0])
45+
return obj
46+
47+
48+
def decode_list(b):
49+
"""
50+
Given a non-deserializable object, make a best effort to
51+
return a useful set of results.
52+
"""
53+
if isinstance(b, list):
54+
return [nativestr(obj) for obj in b]
55+
elif isinstance(b, bytes):
56+
return unstring(nativestr(b))
57+
elif isinstance(b, str):
58+
return unstring(b)
59+
return b

redis/commands/json/helpers.py

-25
This file was deleted.

tests/conftest.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,8 @@ def teardown():
126126
@pytest.fixture()
127127
def modclient(request, **kwargs):
128128
rmurl = request.config.getoption('--redismod-url')
129-
with _get_client(redis.Redis, request, from_url=rmurl, **kwargs) as client:
129+
with _get_client(redis.Redis, request, from_url=rmurl,
130+
decode_responses=True, **kwargs) as client:
130131
yield client
131132

132133

0 commit comments

Comments
 (0)