Skip to content

fix inconsistent json.get behavior #2962

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 2 commits into from
Closed
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
15 changes: 13 additions & 2 deletions redis/asyncio/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@
from redis.asyncio.retry import Retry
from redis.client import (
EMPTY_RESPONSE,
IGNORE_RESPONSE_CALLBACKS,
JSON_GET_COMMAND_NAME_LOWER_CASE,
NEVER_DECODE,
AbstractRedis,
CaseInsensitiveDict,
Expand Down Expand Up @@ -634,12 +636,21 @@ async def parse_response(
if EMPTY_RESPONSE in options:
options.pop(EMPTY_RESPONSE)

if command_name in self.response_callbacks:
if (
command_name.lower() == JSON_GET_COMMAND_NAME_LOWER_CASE
and IGNORE_RESPONSE_CALLBACKS not in options
):
ignore_response_callbacks = True
else:
ignore_response_callbacks = options.pop(IGNORE_RESPONSE_CALLBACKS, False)

if command_name not in self.response_callbacks or ignore_response_callbacks:
return response
else:
# Mypy bug: https://github.com/python/mypy/issues/10977
command_name = cast(str, command_name)
retval = self.response_callbacks[command_name](response, **options)
return await retval if inspect.isawaitable(retval) else retval
return response


StrictRedis = Redis
Expand Down
27 changes: 23 additions & 4 deletions redis/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,9 @@
)

SYM_EMPTY = b""
IGNORE_RESPONSE_CALLBACKS = "ignore_response_callbacks"
EMPTY_RESPONSE = "EMPTY_RESPONSE"
JSON_GET_COMMAND_NAME_LOWER_CASE = "json.get"

# some responses (ie. dump) are binary, and just meant to never be decoded
NEVER_DECODE = "NEVER_DECODE"
Expand Down Expand Up @@ -559,9 +561,18 @@ def parse_response(self, connection, command_name, **options):
if EMPTY_RESPONSE in options:
options.pop(EMPTY_RESPONSE)

if command_name in self.response_callbacks:
return self.response_callbacks[command_name](response, **options)
return response
if (
command_name.lower() == JSON_GET_COMMAND_NAME_LOWER_CASE
and IGNORE_RESPONSE_CALLBACKS not in options
):
ignore_response_callbacks = True
else:
ignore_response_callbacks = options.pop(IGNORE_RESPONSE_CALLBACKS, False)

if command_name not in self.response_callbacks or ignore_response_callbacks:
return response

return self.response_callbacks[command_name](response, **options)


StrictRedis = Redis
Expand Down Expand Up @@ -1377,7 +1388,15 @@ def _execute_transaction(self, connection, commands, raise_on_error) -> List:
if not isinstance(r, Exception):
args, options = cmd
command_name = args[0]
if command_name in self.response_callbacks:
ignore_response_callbacks = options.pop(
IGNORE_RESPONSE_CALLBACKS,
False,
)

if (
command_name in self.response_callbacks
and not ignore_response_callbacks
):
r = self.response_callbacks[command_name](r, **options)
data.append(r)
return data
Expand Down
25 changes: 21 additions & 4 deletions redis/cluster.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from redis._parsers import CommandsParser, Encoder
from redis._parsers.helpers import parse_scan
from redis.backoff import default_backoff
from redis.client import CaseInsensitiveDict, PubSub, Redis
from redis.client import IGNORE_RESPONSE_CALLBACKS, CaseInsensitiveDict, PubSub, Redis
from redis.commands import READ_COMMANDS, RedisClusterCommands
from redis.commands.helpers import list_or_args
from redis.connection import ConnectionPool, DefaultParser, parse_url
Expand Down Expand Up @@ -1064,6 +1064,7 @@ def execute_command(self, *args, **kwargs):
is_default_node = False
target_nodes = None
passed_targets = kwargs.pop("target_nodes", None)
ignore_response_callbacks = kwargs.pop(IGNORE_RESPONSE_CALLBACKS, False)
if passed_targets is not None and not self._is_nodes_flag(passed_targets):
target_nodes = self._parse_target_nodes(passed_targets)
target_nodes_specified = True
Expand Down Expand Up @@ -1098,7 +1099,12 @@ def execute_command(self, *args, **kwargs):
):
is_default_node = True
for node in target_nodes:
res[node.name] = self._execute_command(node, *args, **kwargs)
res[node.name] = self._execute_command(
node,
*args,
**kwargs,
ignore_response_callbacks=ignore_response_callbacks,
)
# Return the processed result
return self._process_result(args[0], res, **kwargs)
except Exception as e:
Expand All @@ -1119,6 +1125,7 @@ def _execute_command(self, target_node, *args, **kwargs):
Send a command to a node in the cluster
"""
command = args[0]
ignore_response_callbacks = kwargs.pop(IGNORE_RESPONSE_CALLBACKS, False)
redis_node = None
connection = None
redirect_addr = None
Expand Down Expand Up @@ -1149,7 +1156,10 @@ def _execute_command(self, target_node, *args, **kwargs):

connection.send_command(*args)
response = redis_node.parse_response(connection, command, **kwargs)
if command in self.cluster_response_callbacks:
if (
command in self.cluster_response_callbacks
and not ignore_response_callbacks
):
response = self.cluster_response_callbacks[command](
response, **kwargs
)
Expand Down Expand Up @@ -2230,7 +2240,14 @@ def _send_cluster_commands(
# to the sequence of commands issued in the stack in pipeline.execute()
response = []
for c in sorted(stack, key=lambda x: x.position):
if c.args[0] in self.cluster_response_callbacks:
ignore_response_callbacks = c.options.pop(
IGNORE_RESPONSE_CALLBACKS,
False,
)
if (
c.args[0] in self.cluster_response_callbacks
and not ignore_response_callbacks
):
c.result = self.cluster_response_callbacks[c.args[0]](
c.result, **c.options
)
Expand Down
14 changes: 12 additions & 2 deletions redis/commands/json/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -172,14 +172,20 @@ def delete(self, key: str, path: Optional[str] = Path.root_path()) -> int:
forget = delete

def get(
self, name: str, *args, no_escape: Optional[bool] = False
self,
name: str,
*args,
no_escape: Optional[bool] = False,
ignore_response_callbacks: Optional[bool] = False,
) -> List[JsonType]:
"""
Get the object stored as a JSON value at key ``name``.

``args`` is zero or more paths, and defaults to root path
```no_escape`` is a boolean flag to add no_escape option to get
non-ascii characters
``ignore_response_callbacks`` is a boolean flag to ignore response callbacks
or not

For more information see `JSON.GET <https://redis.io/commands/json.get>`_.
""" # noqa
Expand All @@ -197,7 +203,11 @@ def get(
# Handle case where key doesn't exist. The JSONDecoder would raise a
# TypeError exception since it can't decode None
try:
return self.execute_command("JSON.GET", *pieces)
return self.execute_command(
"JSON.GET",
*pieces,
ignore_response_callbacks=ignore_response_callbacks,
)
except TypeError:
return None

Expand Down
12 changes: 12 additions & 0 deletions tests/test_json.py
Original file line number Diff line number Diff line change
Expand Up @@ -1525,3 +1525,15 @@ def test_set_path(client):
assert_resp_response(
client, client.json().get(jsonfile.rsplit(".")[0]), res, [[res]]
)


@pytest.mark.onlynoncluster
def test_json_get_response_callbacks_behaviour(client):
bar = {
"qwe": "rty",
"asd": 123,
}
client.json().set("foo", Path.root_path(), bar)
assert isinstance(client.execute_command("JSON.GET", "foo"), str)
assert isinstance(client.json().get("foo"), dict)
assert isinstance(client.execute_command("JSON.GET", "foo"), str)