Skip to content

Commit

Permalink
Improved JSON accuracy (redis#1666)
Browse files Browse the repository at this point in the history
  • Loading branch information
chayim authored and barshaul committed Nov 3, 2021
1 parent ac378a9 commit 62a8956
Show file tree
Hide file tree
Showing 9 changed files with 186 additions and 80 deletions.
2 changes: 2 additions & 0 deletions redis/commands/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ def nativestr(x):

def delist(x):
"""Given a list of binaries, return the stringified version."""
if x is None:
return x
return [nativestr(obj) for obj in x]


Expand Down
8 changes: 6 additions & 2 deletions redis/commands/json/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
from json import JSONDecoder, JSONEncoder

from .decoders import (
int_or_list,
int_or_none
)
from .helpers import bulk_of_jsons
from ..helpers import nativestr, delist
from .commands import JSONCommands
Expand Down Expand Up @@ -48,13 +52,13 @@ def __init__(
"JSON.ARRAPPEND": int,
"JSON.ARRINDEX": int,
"JSON.ARRINSERT": int,
"JSON.ARRLEN": int,
"JSON.ARRLEN": int_or_none,
"JSON.ARRPOP": self._decode,
"JSON.ARRTRIM": int,
"JSON.OBJLEN": int,
"JSON.OBJKEYS": delist,
# "JSON.RESP": delist,
"JSON.DEBUG": int,
"JSON.DEBUG": int_or_list,
}

self.client = client
Expand Down
93 changes: 55 additions & 38 deletions redis/commands/json/commands.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from .path import Path, str_path
from .path import Path
from .helpers import decode_dict_keys
from deprecated import deprecated
from redis.exceptions import DataError


class JSONCommands:
Expand All @@ -9,7 +11,7 @@ def arrappend(self, name, path=Path.rootPath(), *args):
"""Append the objects ``args`` to the array under the
``path` in key ``name``.
"""
pieces = [name, str_path(path)]
pieces = [name, str(path)]
for o in args:
pieces.append(self._encode(o))
return self.execute_command("JSON.ARRAPPEND", *pieces)
Expand All @@ -23,75 +25,72 @@ def arrindex(self, name, path, scalar, start=0, stop=-1):
and exclusive ``stop`` indices.
"""
return self.execute_command(
"JSON.ARRINDEX", name, str_path(path), self._encode(scalar),
"JSON.ARRINDEX", name, str(path), self._encode(scalar),
start, stop
)

def arrinsert(self, name, path, index, *args):
"""Insert the objects ``args`` to the array at index ``index``
under the ``path` in key ``name``.
"""
pieces = [name, str_path(path), index]
pieces = [name, str(path), index]
for o in args:
pieces.append(self._encode(o))
return self.execute_command("JSON.ARRINSERT", *pieces)

def forget(self, name, path=Path.rootPath()):
"""Alias for jsondel (delete the JSON value)."""
return self.execute_command("JSON.FORGET", name, str_path(path))

def arrlen(self, name, path=Path.rootPath()):
"""Return the length of the array JSON value under ``path``
at key``name``.
"""
return self.execute_command("JSON.ARRLEN", name, str_path(path))
return self.execute_command("JSON.ARRLEN", name, str(path))

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

def arrtrim(self, name, path, start, stop):
"""Trim the array JSON value under ``path`` at key ``name`` to the
inclusive range given by ``start`` and ``stop``.
"""
return self.execute_command("JSON.ARRTRIM", name, str_path(path),
return self.execute_command("JSON.ARRTRIM", name, str(path),
start, stop)

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

def resp(self, name, path=Path.rootPath()):
"""Return the JSON value under ``path`` at key ``name``."""
return self.execute_command("JSON.RESP", name, str_path(path))
return self.execute_command("JSON.RESP", name, str(path))

def objkeys(self, name, path=Path.rootPath()):
"""Return the key names in the dictionary JSON value under ``path`` at
key ``name``."""
return self.execute_command("JSON.OBJKEYS", name, str_path(path))
return self.execute_command("JSON.OBJKEYS", name, str(path))

def objlen(self, name, path=Path.rootPath()):
"""Return the length of the dictionary JSON value under ``path`` at key
``name``.
"""
return self.execute_command("JSON.OBJLEN", name, str_path(path))
return self.execute_command("JSON.OBJLEN", name, str(path))

def numincrby(self, name, path, number):
"""Increment the numeric (integer or floating point) JSON value under
``path`` at key ``name`` by the provided ``number``.
"""
return self.execute_command(
"JSON.NUMINCRBY", name, str_path(path), self._encode(number)
"JSON.NUMINCRBY", name, str(path), self._encode(number)
)

@deprecated(version='4.0.0', reason='deprecated since redisjson 1.0.0')
def nummultby(self, name, path, number):
"""Multiply the numeric (integer or floating point) JSON value under
``path`` at key ``name`` with the provided ``number``.
"""
return self.execute_command(
"JSON.NUMMULTBY", name, str_path(path), self._encode(number)
"JSON.NUMMULTBY", name, str(path), self._encode(number)
)

def clear(self, name, path=Path.rootPath()):
Expand All @@ -102,11 +101,14 @@ def clear(self, name, path=Path.rootPath()):
Return the count of cleared paths (ignoring non-array and non-objects
paths).
"""
return self.execute_command("JSON.CLEAR", name, str_path(path))
return self.execute_command("JSON.CLEAR", name, str(path))

def delete(self, key, path=Path.rootPath()):
"""Delete the JSON value stored at key ``key`` under ``path``."""
return self.execute_command("JSON.DEL", key, str(path))

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

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

else:
for p in args:
pieces.append(str_path(p))
pieces.append(str(p))

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

def mget(self, path, *args):
"""Get the objects stored as a JSON values under ``path`` from keys
``args``.
def mget(self, keys, path):
"""
Get the objects stored as a JSON values under ``path``. ``keys``
is a list of one or more keys.
"""
pieces = []
pieces.extend(args)
pieces.append(str_path(path))
pieces += keys
pieces.append(str(path))
return self.execute_command("JSON.MGET", *pieces)

def set(self, name, path, obj, nx=False, xx=False, decode_keys=False):
Expand All @@ -155,7 +158,7 @@ def set(self, name, path, obj, nx=False, xx=False, decode_keys=False):
if decode_keys:
obj = decode_dict_keys(obj)

pieces = [name, str_path(path), self._encode(obj)]
pieces = [name, str(path), self._encode(obj)]

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

def strlen(self, name, path=Path.rootPath()):
def strlen(self, name, path=None):
"""Return the length of the string JSON value under ``path`` at key
``name``.
"""
return self.execute_command("JSON.STRLEN", name, str_path(path))
pieces = [name]
if path is not None:
pieces.append(str(path))
return self.execute_command("JSON.STRLEN", *pieces)

def toggle(self, name, path=Path.rootPath()):
"""Toggle boolean value under ``path`` at key ``name``.
returning the new value.
"""
return self.execute_command("JSON.TOGGLE", name, str_path(path))
return self.execute_command("JSON.TOGGLE", name, str(path))

def strappend(self, name, string, path=Path.rootPath()):
"""Append to the string JSON value under ``path`` at key ``name``
the provided ``string``.
def strappend(self, name, value, path=Path.rootPath()):
"""Append to the string JSON value. If two options are specified after
the key name, the path is determined to be the first. If a single
option is passed, then the rootpath (i.e Path.rootPath()) is used.
"""
pieces = [name, str(path), value]
return self.execute_command(
"JSON.STRAPPEND", name, str_path(path), self._encode(string)
"JSON.STRAPPEND", *pieces
)

def debug(self, name, path=Path.rootPath()):
def debug(self, subcommand, key=None, path=Path.rootPath()):
"""Return the memory usage in bytes of a value under ``path`` from
key ``name``.
"""
return self.execute_command("JSON.DEBUG", "MEMORY",
name, str_path(path))
valid_subcommands = ["MEMORY", "HELP"]
if subcommand not in valid_subcommands:
raise DataError("The only valid subcommands are ",
str(valid_subcommands))
pieces = [subcommand]
if subcommand == "MEMORY":
if key is None:
raise DataError("No key specified")
pieces.append(key)
pieces.append(str(path))
return self.execute_command("JSON.DEBUG", *pieces)
12 changes: 12 additions & 0 deletions redis/commands/json/decoders.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
def int_or_list(b):
if isinstance(b, int):
return b
else:
return b


def int_or_none(b):
if b is None:
return None
if isinstance(b, int):
return b
11 changes: 3 additions & 8 deletions redis/commands/json/path.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,3 @@
def str_path(p):
"""Return the string representation of a path if it is of class Path."""
if isinstance(p, Path):
return p.strPath
else:
return p


class Path(object):
"""This class represents a path in a JSON value."""

Expand All @@ -19,3 +11,6 @@ def rootPath():
def __init__(self, path):
"""Make a new path based on the string representation in `path`."""
self.strPath = path

def __repr__(self):
return self.strPath
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
deprecated
3 changes: 3 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@
author="Redis Inc.",
author_email="oss@redis.com",
python_requires=">=3.6",
install_requires=[
'deprecated'
],
classifiers=[
"Development Status :: 5 - Production/Stable",
"Environment :: Console",
Expand Down
Loading

0 comments on commit 62a8956

Please sign in to comment.