Skip to content

Commit 75c5d59

Browse files
authored
Improved JSON accuracy (#1666)
1 parent e46dd85 commit 75c5d59

File tree

9 files changed

+165
-76
lines changed

9 files changed

+165
-76
lines changed

redis/commands/helpers.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ def nativestr(x):
2222

2323
def delist(x):
2424
"""Given a list of binaries, return the stringified version."""
25+
if x is None:
26+
return x
2527
return [nativestr(obj) for obj in x]
2628

2729

redis/commands/json/__init__.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
from json import JSONDecoder, JSONEncoder
22

3+
from .decoders import (
4+
int_or_list,
5+
int_or_none
6+
)
37
from .helpers import bulk_of_jsons
48
from ..helpers import nativestr, delist
59
from .commands import JSONCommands
@@ -48,13 +52,13 @@ def __init__(
4852
"JSON.ARRAPPEND": int,
4953
"JSON.ARRINDEX": int,
5054
"JSON.ARRINSERT": int,
51-
"JSON.ARRLEN": int,
55+
"JSON.ARRLEN": int_or_none,
5256
"JSON.ARRPOP": self._decode,
5357
"JSON.ARRTRIM": int,
5458
"JSON.OBJLEN": int,
5559
"JSON.OBJKEYS": delist,
5660
# "JSON.RESP": delist,
57-
"JSON.DEBUG": int,
61+
"JSON.DEBUG": int_or_list,
5862
}
5963

6064
self.client = client

redis/commands/json/commands.py

Lines changed: 55 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1-
from .path import Path, str_path
1+
from .path import Path
22
from .helpers import decode_dict_keys
3+
from deprecated import deprecated
4+
from redis.exceptions import DataError
35

46

57
class JSONCommands:
@@ -9,7 +11,7 @@ def arrappend(self, name, path=Path.rootPath(), *args):
911
"""Append the objects ``args`` to the array under the
1012
``path` in key ``name``.
1113
"""
12-
pieces = [name, str_path(path)]
14+
pieces = [name, str(path)]
1315
for o in args:
1416
pieces.append(self._encode(o))
1517
return self.execute_command("JSON.ARRAPPEND", *pieces)
@@ -23,75 +25,72 @@ def arrindex(self, name, path, scalar, start=0, stop=-1):
2325
and exclusive ``stop`` indices.
2426
"""
2527
return self.execute_command(
26-
"JSON.ARRINDEX", name, str_path(path), self._encode(scalar),
28+
"JSON.ARRINDEX", name, str(path), self._encode(scalar),
2729
start, stop
2830
)
2931

3032
def arrinsert(self, name, path, index, *args):
3133
"""Insert the objects ``args`` to the array at index ``index``
3234
under the ``path` in key ``name``.
3335
"""
34-
pieces = [name, str_path(path), index]
36+
pieces = [name, str(path), index]
3537
for o in args:
3638
pieces.append(self._encode(o))
3739
return self.execute_command("JSON.ARRINSERT", *pieces)
3840

39-
def forget(self, name, path=Path.rootPath()):
40-
"""Alias for jsondel (delete the JSON value)."""
41-
return self.execute_command("JSON.FORGET", name, str_path(path))
42-
4341
def arrlen(self, name, path=Path.rootPath()):
4442
"""Return the length of the array JSON value under ``path``
4543
at key``name``.
4644
"""
47-
return self.execute_command("JSON.ARRLEN", name, str_path(path))
45+
return self.execute_command("JSON.ARRLEN", name, str(path))
4846

4947
def arrpop(self, name, path=Path.rootPath(), index=-1):
5048
"""Pop the element at ``index`` in the array JSON value under
5149
``path`` at key ``name``.
5250
"""
53-
return self.execute_command("JSON.ARRPOP", name, str_path(path), index)
51+
return self.execute_command("JSON.ARRPOP", name, str(path), index)
5452

5553
def arrtrim(self, name, path, start, stop):
5654
"""Trim the array JSON value under ``path`` at key ``name`` to the
5755
inclusive range given by ``start`` and ``stop``.
5856
"""
59-
return self.execute_command("JSON.ARRTRIM", name, str_path(path),
57+
return self.execute_command("JSON.ARRTRIM", name, str(path),
6058
start, stop)
6159

6260
def type(self, name, path=Path.rootPath()):
6361
"""Get the type of the JSON value under ``path`` from key ``name``."""
64-
return self.execute_command("JSON.TYPE", name, str_path(path))
62+
return self.execute_command("JSON.TYPE", name, str(path))
6563

6664
def resp(self, name, path=Path.rootPath()):
6765
"""Return the JSON value under ``path`` at key ``name``."""
68-
return self.execute_command("JSON.RESP", name, str_path(path))
66+
return self.execute_command("JSON.RESP", name, str(path))
6967

7068
def objkeys(self, name, path=Path.rootPath()):
7169
"""Return the key names in the dictionary JSON value under ``path`` at
7270
key ``name``."""
73-
return self.execute_command("JSON.OBJKEYS", name, str_path(path))
71+
return self.execute_command("JSON.OBJKEYS", name, str(path))
7472

7573
def objlen(self, name, path=Path.rootPath()):
7674
"""Return the length of the dictionary JSON value under ``path`` at key
7775
``name``.
7876
"""
79-
return self.execute_command("JSON.OBJLEN", name, str_path(path))
77+
return self.execute_command("JSON.OBJLEN", name, str(path))
8078

8179
def numincrby(self, name, path, number):
8280
"""Increment the numeric (integer or floating point) JSON value under
8381
``path`` at key ``name`` by the provided ``number``.
8482
"""
8583
return self.execute_command(
86-
"JSON.NUMINCRBY", name, str_path(path), self._encode(number)
84+
"JSON.NUMINCRBY", name, str(path), self._encode(number)
8785
)
8886

87+
@deprecated(version='4.0.0', reason='deprecated since redisjson 1.0.0')
8988
def nummultby(self, name, path, number):
9089
"""Multiply the numeric (integer or floating point) JSON value under
9190
``path`` at key ``name`` with the provided ``number``.
9291
"""
9392
return self.execute_command(
94-
"JSON.NUMMULTBY", name, str_path(path), self._encode(number)
93+
"JSON.NUMMULTBY", name, str(path), self._encode(number)
9594
)
9695

9796
def clear(self, name, path=Path.rootPath()):
@@ -102,11 +101,14 @@ def clear(self, name, path=Path.rootPath()):
102101
Return the count of cleared paths (ignoring non-array and non-objects
103102
paths).
104103
"""
105-
return self.execute_command("JSON.CLEAR", name, str_path(path))
104+
return self.execute_command("JSON.CLEAR", name, str(path))
105+
106+
def delete(self, key, path=Path.rootPath()):
107+
"""Delete the JSON value stored at key ``key`` under ``path``."""
108+
return self.execute_command("JSON.DEL", key, str(path))
106109

107-
def delete(self, name, path=Path.rootPath()):
108-
"""Delete the JSON value stored at key ``name`` under ``path``."""
109-
return self.execute_command("JSON.DEL", name, str_path(path))
110+
# forget is an alias for delete
111+
forget = delete
110112

111113
def get(self, name, *args, no_escape=False):
112114
"""
@@ -125,7 +127,7 @@ def get(self, name, *args, no_escape=False):
125127

126128
else:
127129
for p in args:
128-
pieces.append(str_path(p))
130+
pieces.append(str(p))
129131

130132
# Handle case where key doesn't exist. The JSONDecoder would raise a
131133
# TypeError exception since it can't decode None
@@ -134,13 +136,14 @@ def get(self, name, *args, no_escape=False):
134136
except TypeError:
135137
return None
136138

137-
def mget(self, path, *args):
138-
"""Get the objects stored as a JSON values under ``path`` from keys
139-
``args``.
139+
def mget(self, keys, path):
140+
"""
141+
Get the objects stored as a JSON values under ``path``. ``keys``
142+
is a list of one or more keys.
140143
"""
141144
pieces = []
142-
pieces.extend(args)
143-
pieces.append(str_path(path))
145+
pieces += keys
146+
pieces.append(str(path))
144147
return self.execute_command("JSON.MGET", *pieces)
145148

146149
def set(self, name, path, obj, nx=False, xx=False, decode_keys=False):
@@ -155,7 +158,7 @@ def set(self, name, path, obj, nx=False, xx=False, decode_keys=False):
155158
if decode_keys:
156159
obj = decode_dict_keys(obj)
157160

158-
pieces = [name, str_path(path), self._encode(obj)]
161+
pieces = [name, str(path), self._encode(obj)]
159162

160163
# Handle existential modifiers
161164
if nx and xx:
@@ -169,29 +172,43 @@ def set(self, name, path, obj, nx=False, xx=False, decode_keys=False):
169172
pieces.append("XX")
170173
return self.execute_command("JSON.SET", *pieces)
171174

172-
def strlen(self, name, path=Path.rootPath()):
175+
def strlen(self, name, path=None):
173176
"""Return the length of the string JSON value under ``path`` at key
174177
``name``.
175178
"""
176-
return self.execute_command("JSON.STRLEN", name, str_path(path))
179+
pieces = [name]
180+
if path is not None:
181+
pieces.append(str(path))
182+
return self.execute_command("JSON.STRLEN", *pieces)
177183

178184
def toggle(self, name, path=Path.rootPath()):
179185
"""Toggle boolean value under ``path`` at key ``name``.
180186
returning the new value.
181187
"""
182-
return self.execute_command("JSON.TOGGLE", name, str_path(path))
188+
return self.execute_command("JSON.TOGGLE", name, str(path))
183189

184-
def strappend(self, name, string, path=Path.rootPath()):
185-
"""Append to the string JSON value under ``path`` at key ``name``
186-
the provided ``string``.
190+
def strappend(self, name, value, path=Path.rootPath()):
191+
"""Append to the string JSON value. If two options are specified after
192+
the key name, the path is determined to be the first. If a single
193+
option is passed, then the rootpath (i.e Path.rootPath()) is used.
187194
"""
195+
pieces = [name, str(path), value]
188196
return self.execute_command(
189-
"JSON.STRAPPEND", name, str_path(path), self._encode(string)
197+
"JSON.STRAPPEND", *pieces
190198
)
191199

192-
def debug(self, name, path=Path.rootPath()):
200+
def debug(self, subcommand, key=None, path=Path.rootPath()):
193201
"""Return the memory usage in bytes of a value under ``path`` from
194202
key ``name``.
195203
"""
196-
return self.execute_command("JSON.DEBUG", "MEMORY",
197-
name, str_path(path))
204+
valid_subcommands = ["MEMORY", "HELP"]
205+
if subcommand not in valid_subcommands:
206+
raise DataError("The only valid subcommands are ",
207+
str(valid_subcommands))
208+
pieces = [subcommand]
209+
if subcommand == "MEMORY":
210+
if key is None:
211+
raise DataError("No key specified")
212+
pieces.append(key)
213+
pieces.append(str(path))
214+
return self.execute_command("JSON.DEBUG", *pieces)

redis/commands/json/decoders.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
def int_or_list(b):
2+
if isinstance(b, int):
3+
return b
4+
else:
5+
return b
6+
7+
8+
def int_or_none(b):
9+
if b is None:
10+
return None
11+
if isinstance(b, int):
12+
return b

redis/commands/json/path.py

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,3 @@
1-
def str_path(p):
2-
"""Return the string representation of a path if it is of class Path."""
3-
if isinstance(p, Path):
4-
return p.strPath
5-
else:
6-
return p
7-
8-
91
class Path(object):
102
"""This class represents a path in a JSON value."""
113

@@ -19,3 +11,6 @@ def rootPath():
1911
def __init__(self, path):
2012
"""Make a new path based on the string representation in `path`."""
2113
self.strPath = path
14+
15+
def __repr__(self):
16+
return self.strPath

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
deprecated

setup.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@
2323
author="Redis Inc.",
2424
author_email="[email protected]",
2525
python_requires=">=3.6",
26+
install_requires=[
27+
'deprecated'
28+
],
2629
classifiers=[
2730
"Development Status :: 5 - Production/Stable",
2831
"Environment :: Console",

0 commit comments

Comments
 (0)