diff --git a/.github/workflows/docs.yaml b/.github/workflows/docs.yaml index c5c74aa4d3..a3512b46dc 100644 --- a/.github/workflows/docs.yaml +++ b/.github/workflows/docs.yaml @@ -13,7 +13,7 @@ on: - cron: '0 1 * * *' # nightly build concurrency: - group: ${{ github.event.pull_request.number || github.ref }} + group: ${{ github.event.pull_request.number || github.ref }}-docs cancel-in-progress: true permissions: diff --git a/.github/workflows/integration.yaml b/.github/workflows/integration.yaml index ae2cfd6cfc..7155983560 100644 --- a/.github/workflows/integration.yaml +++ b/.github/workflows/integration.yaml @@ -17,7 +17,7 @@ on: - cron: '0 1 * * *' # nightly build concurrency: - group: ${{ github.event.pull_request.number || github.ref }} + group: ${{ github.event.pull_request.number || github.ref }}-integration cancel-in-progress: true permissions: diff --git a/dockers/cluster.redis.conf b/dockers/cluster.redis.conf index d4de46fbed..aee81f648a 100644 --- a/dockers/cluster.redis.conf +++ b/dockers/cluster.redis.conf @@ -1,7 +1,6 @@ protected-mode no enable-debug-command yes loadmodule /opt/redis-stack/lib/redisearch.so -loadmodule /opt/redis-stack/lib/redisgraph.so loadmodule /opt/redis-stack/lib/redistimeseries.so loadmodule /opt/redis-stack/lib/rejson.so loadmodule /opt/redis-stack/lib/redisbloom.so diff --git a/redis/_parsers/helpers.py b/redis/_parsers/helpers.py index 7418cdca53..9dc8bd6c8b 100644 --- a/redis/_parsers/helpers.py +++ b/redis/_parsers/helpers.py @@ -46,11 +46,18 @@ def get_value(value): return int(value) except ValueError: return value + elif "=" not in value: + return [get_value(v) for v in value.split(",") if v] else: sub_dict = {} for item in value.split(","): - k, v = item.rsplit("=", 1) - sub_dict[k] = get_value(v) + if not item: + continue + if "=" in item: + k, v = item.rsplit("=", 1) + sub_dict[k] = get_value(v) + else: + sub_dict[item] = True return sub_dict for line in response.splitlines(): @@ -80,7 +87,7 @@ def parse_memory_stats(response, **kwargs): """Parse the results of MEMORY STATS""" stats = pairs_to_dict(response, decode_keys=True, decode_string_values=True) for key, value in stats.items(): - if key.startswith("db."): + if key.startswith("db.") and isinstance(value, list): stats[key] = pairs_to_dict( value, decode_keys=True, decode_string_values=True ) diff --git a/redis/commands/helpers.py b/redis/commands/helpers.py index 127141f650..1ea02a60cf 100644 --- a/redis/commands/helpers.py +++ b/redis/commands/helpers.py @@ -85,7 +85,11 @@ def parse_to_dict(response): res = {} for det in response: - if isinstance(det[1], list): + if not isinstance(det, list) or not det: + continue + if len(det) == 1: + res[det[0]] = True + elif isinstance(det[1], list): res[det[0]] = parse_list_to_dict(det[1]) else: try: # try to set the attribute. may be provided without value diff --git a/setup.py b/setup.py index c54da83f47..4a157ea150 100644 --- a/setup.py +++ b/setup.py @@ -8,7 +8,7 @@ long_description_content_type="text/markdown", keywords=["Redis", "key-value store", "database"], license="MIT", - version="5.0.5", + version="5.0.6", packages=find_packages( include=[ "redis", diff --git a/tasks.py b/tasks.py index c60fa2791e..7f26081150 100644 --- a/tasks.py +++ b/tasks.py @@ -13,7 +13,7 @@ def devenv(c): """Brings up the test environment, by wrapping docker compose.""" clean(c) - cmd = "docker-compose --profile all up -d" + cmd = "docker-compose --profile all up -d --build" run(cmd) diff --git a/tests/test_asyncio/test_cluster.py b/tests/test_asyncio/test_cluster.py index d2f165b5f5..d0b92fb4a6 100644 --- a/tests/test_asyncio/test_cluster.py +++ b/tests/test_asyncio/test_cluster.py @@ -1445,7 +1445,7 @@ async def test_memory_stats(self, r: RedisCluster) -> None: assert isinstance(stats, dict) for key, value in stats.items(): if key.startswith("db."): - assert isinstance(value, dict) + assert not isinstance(value, list) @skip_if_server_version_lt("4.0.0") async def test_memory_help(self, r: RedisCluster) -> None: diff --git a/tests/test_asyncio/test_commands.py b/tests/test_asyncio/test_commands.py index 35b9f2a29f..9ed7b84184 100644 --- a/tests/test_asyncio/test_commands.py +++ b/tests/test_asyncio/test_commands.py @@ -3207,7 +3207,7 @@ async def test_memory_stats(self, r: redis.Redis): assert isinstance(stats, dict) for key, value in stats.items(): if key.startswith("db."): - assert isinstance(value, dict) + assert not isinstance(value, list) @skip_if_server_version_lt("4.0.0") async def test_memory_usage(self, r: redis.Redis): diff --git a/tests/test_asyncio/test_graph.py b/tests/test_asyncio/test_graph.py index 4caf79470e..d1649b617b 100644 --- a/tests/test_asyncio/test_graph.py +++ b/tests/test_asyncio/test_graph.py @@ -12,6 +12,8 @@ async def test_bulk(decoded_r): await decoded_r.graph().bulk(foo="bar!") +@pytest.mark.redismod +@pytest.mark.skip(reason="Graph module removed from Redis Stack") async def test_graph_creation(decoded_r: redis.Redis): graph = decoded_r.graph() @@ -56,6 +58,8 @@ async def test_graph_creation(decoded_r: redis.Redis): await graph.delete() +@pytest.mark.redismod +@pytest.mark.skip(reason="Graph module removed from Redis Stack") async def test_array_functions(decoded_r: redis.Redis): graph = decoded_r.graph() @@ -78,6 +82,8 @@ async def test_array_functions(decoded_r: redis.Redis): assert [a] == result.result_set[0][0] +@pytest.mark.redismod +@pytest.mark.skip(reason="Graph module removed from Redis Stack") async def test_path(decoded_r: redis.Redis): node0 = Node(node_id=0, label="L1") node1 = Node(node_id=1, label="L1") @@ -97,6 +103,8 @@ async def test_path(decoded_r: redis.Redis): assert expected_results == result.result_set +@pytest.mark.redismod +@pytest.mark.skip(reason="Graph module removed from Redis Stack") async def test_param(decoded_r: redis.Redis): params = [1, 2.3, "str", True, False, None, [0, 1, 2]] query = "RETURN $param" @@ -106,6 +114,8 @@ async def test_param(decoded_r: redis.Redis): assert expected_results == result.result_set +@pytest.mark.redismod +@pytest.mark.skip(reason="Graph module removed from Redis Stack") async def test_map(decoded_r: redis.Redis): query = "RETURN {a:1, b:'str', c:NULL, d:[1,2,3], e:True, f:{x:1, y:2}}" @@ -122,6 +132,8 @@ async def test_map(decoded_r: redis.Redis): assert actual == expected +@pytest.mark.redismod +@pytest.mark.skip(reason="Graph module removed from Redis Stack") async def test_point(decoded_r: redis.Redis): query = "RETURN point({latitude: 32.070794860, longitude: 34.820751118})" expected_lat = 32.070794860 @@ -138,6 +150,8 @@ async def test_point(decoded_r: redis.Redis): assert abs(actual["longitude"] - expected_lon) < 0.001 +@pytest.mark.redismod +@pytest.mark.skip(reason="Graph module removed from Redis Stack") async def test_index_response(decoded_r: redis.Redis): result_set = await decoded_r.graph().query("CREATE INDEX ON :person(age)") assert 1 == result_set.indices_created @@ -152,6 +166,8 @@ async def test_index_response(decoded_r: redis.Redis): await decoded_r.graph().query("DROP INDEX ON :person(age)") +@pytest.mark.redismod +@pytest.mark.skip(reason="Graph module removed from Redis Stack") async def test_stringify_query_result(decoded_r: redis.Redis): graph = decoded_r.graph() @@ -205,6 +221,8 @@ async def test_stringify_query_result(decoded_r: redis.Redis): await graph.delete() +@pytest.mark.redismod +@pytest.mark.skip(reason="Graph module removed from Redis Stack") async def test_optional_match(decoded_r: redis.Redis): # Build a graph of form (a)-[R]->(b) node0 = Node(node_id=0, label="L1", properties={"value": "a"}) @@ -229,6 +247,8 @@ async def test_optional_match(decoded_r: redis.Redis): await graph.delete() +@pytest.mark.redismod +@pytest.mark.skip(reason="Graph module removed from Redis Stack") async def test_cached_execution(decoded_r: redis.Redis): await decoded_r.graph().query("CREATE ()") @@ -248,6 +268,8 @@ async def test_cached_execution(decoded_r: redis.Redis): assert cached_result.cached_execution +@pytest.mark.redismod +@pytest.mark.skip(reason="Graph module removed from Redis Stack") async def test_slowlog(decoded_r: redis.Redis): create_query = """CREATE (:Rider {name:'Valentino Rossi'})-[:rides]->(:Team {name:'Yamaha'}), @@ -261,6 +283,8 @@ async def test_slowlog(decoded_r: redis.Redis): @pytest.mark.xfail(strict=False) +@pytest.mark.redismod +@pytest.mark.skip(reason="Graph module removed from Redis Stack") async def test_query_timeout(decoded_r: redis.Redis): # Build a sample graph with 1000 nodes. await decoded_r.graph().query("UNWIND range(0,1000) as val CREATE ({v: val})") @@ -274,6 +298,8 @@ async def test_query_timeout(decoded_r: redis.Redis): assert False is False +@pytest.mark.redismod +@pytest.mark.skip(reason="Graph module removed from Redis Stack") async def test_read_only_query(decoded_r: redis.Redis): with pytest.raises(Exception): # Issue a write query, specifying read-only true, @@ -282,6 +308,8 @@ async def test_read_only_query(decoded_r: redis.Redis): assert False is False +@pytest.mark.redismod +@pytest.mark.skip(reason="Graph module removed from Redis Stack") async def test_profile(decoded_r: redis.Redis): q = """UNWIND range(1, 3) AS x CREATE (p:Person {v:x})""" profile = (await decoded_r.graph().profile(q)).result_set @@ -297,6 +325,8 @@ async def test_profile(decoded_r: redis.Redis): @skip_if_redis_enterprise() +@pytest.mark.redismod +@pytest.mark.skip(reason="Graph module removed from Redis Stack") async def test_config(decoded_r: redis.Redis): config_name = "RESULTSET_SIZE" config_value = 3 @@ -328,6 +358,8 @@ async def test_config(decoded_r: redis.Redis): @pytest.mark.onlynoncluster +@pytest.mark.redismod +@pytest.mark.skip(reason="Graph module removed from Redis Stack") async def test_list_keys(decoded_r: redis.Redis): result = await decoded_r.graph().list_keys() assert result == [] @@ -350,6 +382,8 @@ async def test_list_keys(decoded_r: redis.Redis): assert result == [] +@pytest.mark.redismod +@pytest.mark.skip(reason="Graph module removed from Redis Stack") async def test_multi_label(decoded_r: redis.Redis): redis_graph = decoded_r.graph("g") @@ -375,6 +409,8 @@ async def test_multi_label(decoded_r: redis.Redis): assert True +@pytest.mark.redismod +@pytest.mark.skip(reason="Graph module removed from Redis Stack") async def test_execution_plan(decoded_r: redis.Redis): redis_graph = decoded_r.graph("execution_plan") create_query = """CREATE @@ -393,6 +429,8 @@ async def test_execution_plan(decoded_r: redis.Redis): await redis_graph.delete() +@pytest.mark.redismod +@pytest.mark.skip(reason="Graph module removed from Redis Stack") async def test_explain(decoded_r: redis.Redis): redis_graph = decoded_r.graph("execution_plan") # graph creation / population diff --git a/tests/test_asyncio/test_json.py b/tests/test_asyncio/test_json.py index 920ec71dce..81821d931a 100644 --- a/tests/test_asyncio/test_json.py +++ b/tests/test_asyncio/test_json.py @@ -95,6 +95,7 @@ async def test_jsonsetexistentialmodifiersshouldsucceed(decoded_r: redis.Redis): await decoded_r.json().set("obj", Path("foo"), "baz", nx=True, xx=True) +@pytest.mark.onlynoncluster async def test_mgetshouldsucceed(decoded_r: redis.Redis): await decoded_r.json().set("1", Path.root_path(), 1) await decoded_r.json().set("2", Path.root_path(), 2) diff --git a/tests/test_asyncio/test_timeseries.py b/tests/test_asyncio/test_timeseries.py index b44219707e..1302ee4fa2 100644 --- a/tests/test_asyncio/test_timeseries.py +++ b/tests/test_asyncio/test_timeseries.py @@ -175,6 +175,7 @@ async def test_incrby_decrby(decoded_r: redis.Redis): assert_resp_response(decoded_r, 128, info.get("chunk_size"), info.get("chunkSize")) +@pytest.mark.onlynoncluster async def test_create_and_delete_rule(decoded_r: redis.Redis): # test rule creation time = 100 diff --git a/tests/test_cluster.py b/tests/test_cluster.py index 3379da35a8..5383390090 100644 --- a/tests/test_cluster.py +++ b/tests/test_cluster.py @@ -1569,7 +1569,7 @@ def test_memory_stats(self, r): assert isinstance(stats, dict) for key, value in stats.items(): if key.startswith("db."): - assert isinstance(value, dict) + assert not isinstance(value, list) @skip_if_server_version_lt("4.0.0") def test_memory_help(self, r): diff --git a/tests/test_commands.py b/tests/test_commands.py index b2d7c1b9ed..0e93d340f4 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -4880,7 +4880,7 @@ def test_memory_stats(self, r): assert isinstance(stats, dict) for key, value in stats.items(): if key.startswith("db."): - assert isinstance(value, dict) + assert not isinstance(value, list) @skip_if_server_version_lt("4.0.0") def test_memory_usage(self, r): diff --git a/tests/test_graph.py b/tests/test_graph.py index c6d128908e..6007de896b 100644 --- a/tests/test_graph.py +++ b/tests/test_graph.py @@ -30,12 +30,16 @@ def client(request): return r +@pytest.mark.redismod +@pytest.mark.skip(reason="Graph module removed from Redis Stack") def test_bulk(client): with pytest.raises(NotImplementedError): client.graph().bulk() client.graph().bulk(foo="bar!") +@pytest.mark.redismod +@pytest.mark.skip(reason="Graph module removed from Redis Stack") def test_graph_creation(client): graph = client.graph() @@ -80,6 +84,8 @@ def test_graph_creation(client): graph.delete() +@pytest.mark.redismod +@pytest.mark.skip(reason="Graph module removed from Redis Stack") def test_array_functions(client): query = """CREATE (p:person{name:'a',age:32, array:[0,1,2]})""" client.graph().query(query) @@ -100,6 +106,8 @@ def test_array_functions(client): assert [a] == result.result_set[0][0] +@pytest.mark.redismod +@pytest.mark.skip(reason="Graph module removed from Redis Stack") def test_path(client): node0 = Node(node_id=0, label="L1") node1 = Node(node_id=1, label="L1") @@ -119,6 +127,8 @@ def test_path(client): assert expected_results == result.result_set +@pytest.mark.redismod +@pytest.mark.skip(reason="Graph module removed from Redis Stack") def test_param(client): params = [1, 2.3, "str", True, False, None, [0, 1, 2], r"\" RETURN 1337 //"] query = "RETURN $param" @@ -128,6 +138,8 @@ def test_param(client): assert expected_results == result.result_set +@pytest.mark.redismod +@pytest.mark.skip(reason="Graph module removed from Redis Stack") def test_map(client): query = "RETURN {a:1, b:'str', c:NULL, d:[1,2,3], e:True, f:{x:1, y:2}}" @@ -144,6 +156,8 @@ def test_map(client): assert actual == expected +@pytest.mark.redismod +@pytest.mark.skip(reason="Graph module removed from Redis Stack") def test_point(client): query = "RETURN point({latitude: 32.070794860, longitude: 34.820751118})" expected_lat = 32.070794860 @@ -160,6 +174,8 @@ def test_point(client): assert abs(actual["longitude"] - expected_lon) < 0.001 +@pytest.mark.redismod +@pytest.mark.skip(reason="Graph module removed from Redis Stack") def test_index_response(client): result_set = client.graph().query("CREATE INDEX ON :person(age)") assert 1 == result_set.indices_created @@ -174,6 +190,8 @@ def test_index_response(client): client.graph().query("DROP INDEX ON :person(age)") +@pytest.mark.redismod +@pytest.mark.skip(reason="Graph module removed from Redis Stack") def test_stringify_query_result(client): graph = client.graph() @@ -227,6 +245,8 @@ def test_stringify_query_result(client): graph.delete() +@pytest.mark.redismod +@pytest.mark.skip(reason="Graph module removed from Redis Stack") def test_optional_match(client): # Build a graph of form (a)-[R]->(b) node0 = Node(node_id=0, label="L1", properties={"value": "a"}) @@ -251,6 +271,8 @@ def test_optional_match(client): graph.delete() +@pytest.mark.redismod +@pytest.mark.skip(reason="Graph module removed from Redis Stack") def test_cached_execution(client): client.graph().query("CREATE ()") @@ -268,6 +290,8 @@ def test_cached_execution(client): assert cached_result.cached_execution +@pytest.mark.redismod +@pytest.mark.skip(reason="Graph module removed from Redis Stack") def test_slowlog(client): create_query = """CREATE (:Rider {name:'Valentino Rossi'})-[:rides]->(:Team {name:'Yamaha'}), @@ -280,7 +304,9 @@ def test_slowlog(client): assert results[0][2] == create_query +@pytest.mark.redismod @pytest.mark.xfail(strict=False) +@pytest.mark.skip(reason="Graph module removed from Redis Stack") def test_query_timeout(client): # Build a sample graph with 1000 nodes. client.graph().query("UNWIND range(0,1000) as val CREATE ({v: val})") @@ -294,6 +320,8 @@ def test_query_timeout(client): assert False is False +@pytest.mark.redismod +@pytest.mark.skip(reason="Graph module removed from Redis Stack") def test_read_only_query(client): with pytest.raises(Exception): # Issue a write query, specifying read-only true, @@ -302,6 +330,8 @@ def test_read_only_query(client): assert False is False +@pytest.mark.redismod +@pytest.mark.skip(reason="Graph module removed from Redis Stack") def test_profile(client): q = """UNWIND range(1, 3) AS x CREATE (p:Person {v:x})""" profile = client.graph().profile(q).result_set @@ -316,7 +346,9 @@ def test_profile(client): assert "Node By Label Scan | (p:Person) | Records produced: 3" in profile +@pytest.mark.redismod @skip_if_redis_enterprise() +@pytest.mark.skip(reason="Graph module removed from Redis Stack") def test_config(client): config_name = "RESULTSET_SIZE" config_value = 3 @@ -348,6 +380,8 @@ def test_config(client): @pytest.mark.onlynoncluster +@pytest.mark.redismod +@pytest.mark.skip(reason="Graph module removed from Redis Stack") def test_list_keys(client): result = client.graph().list_keys() assert result == [] @@ -370,6 +404,8 @@ def test_list_keys(client): assert result == [] +@pytest.mark.redismod +@pytest.mark.skip(reason="Graph module removed from Redis Stack") def test_multi_label(client): redis_graph = client.graph("g") @@ -395,6 +431,8 @@ def test_multi_label(client): assert True +@pytest.mark.redismod +@pytest.mark.skip(reason="Graph module removed from Redis Stack") def test_cache_sync(client): pass return @@ -467,6 +505,8 @@ def test_cache_sync(client): assert A._relationship_types[1] == "R" +@pytest.mark.redismod +@pytest.mark.skip(reason="Graph module removed from Redis Stack") def test_execution_plan(client): redis_graph = client.graph("execution_plan") create_query = """CREATE @@ -485,6 +525,8 @@ def test_execution_plan(client): redis_graph.delete() +@pytest.mark.redismod +@pytest.mark.skip(reason="Graph module removed from Redis Stack") def test_explain(client): redis_graph = client.graph("execution_plan") # graph creation / population @@ -573,6 +615,8 @@ def test_explain(client): redis_graph.delete() +@pytest.mark.redismod +@pytest.mark.skip(reason="Graph module removed from Redis Stack") def test_resultset_statistics(client): with patch.object(target=QueryResult, attribute="_get_stat") as mock_get_stats: result = client.graph().query("RETURN 1") diff --git a/tests/test_json.py b/tests/test_json.py index 73d72b8cc9..00f7e2fce1 100644 --- a/tests/test_json.py +++ b/tests/test_json.py @@ -102,6 +102,7 @@ def test_jsonsetexistentialmodifiersshouldsucceed(client): client.json().set("obj", Path("foo"), "baz", nx=True, xx=True) +@pytest.mark.onlynoncluster def test_mgetshouldsucceed(client): client.json().set("1", Path.root_path(), 1) client.json().set("2", Path.root_path(), 2) diff --git a/tests/test_parsers/test_helpers.py b/tests/test_parsers/test_helpers.py index 6430a237f6..383b9de794 100644 --- a/tests/test_parsers/test_helpers.py +++ b/tests/test_parsers/test_helpers.py @@ -33,3 +33,31 @@ def test_parse_info(): assert info["search_version"] == "99.99.99" assert info["search_redis_version"] == "7.2.2 - oss" assert info["search_query_timeout_ms"] == 500 + + +def test_parse_info_list(): + info_output = """ +list_one:a, +list_two:a b,,c,10,1.1 + """ + info = parse_info(info_output) + + assert isinstance(info["list_one"], list) + assert info["list_one"] == ["a"] + + assert isinstance(info["list_two"], list) + assert info["list_two"] == ["a b", "c", 10, 1.1] + + +def test_parse_info_list_dict_mixed(): + info_output = """ +list_one:a,b=1 +list_two:a b=foo,,c,d=bar,e, + """ + info = parse_info(info_output) + + assert isinstance(info["list_one"], dict) + assert info["list_one"] == {"a": True, "b": 1} + + assert isinstance(info["list_two"], dict) + assert info["list_two"] == {"a b": "foo", "c": True, "d": "bar", "e": True} diff --git a/tests/test_timeseries.py b/tests/test_timeseries.py index 6b59967f3c..60472e0194 100644 --- a/tests/test_timeseries.py +++ b/tests/test_timeseries.py @@ -171,6 +171,7 @@ def test_incrby_decrby(client): assert_resp_response(client, 128, info.get("chunk_size"), info.get("chunkSize")) +@pytest.mark.onlynoncluster def test_create_and_delete_rule(client): # test rule creation time = 100