Skip to content

Commit 9f82778

Browse files
Stralgo (#1528)
* add support to STRALDO command * add tests * skip if version .. * new line * lower case * fix comments * callback * change to get
1 parent 5964d70 commit 9f82778

File tree

3 files changed

+119
-0
lines changed

3 files changed

+119
-0
lines changed

redis/client.py

+29
Original file line numberDiff line numberDiff line change
@@ -410,6 +410,34 @@ def parse_slowlog_get(response, **options):
410410
} for item in response]
411411

412412

413+
def parse_stralgo(response, **options):
414+
"""
415+
Parse the response from `STRALGO` command.
416+
Without modifiers the returned value is string.
417+
When LEN is given the command returns the length of the result
418+
(i.e integer).
419+
When IDX is given the command returns a dictionary with the LCS
420+
length and all the ranges in both the strings, start and end
421+
offset for each string, where there are matches.
422+
When WITHMATCHLEN is given, each array representing a match will
423+
also have the length of the match at the beginning of the array.
424+
"""
425+
if options.get('len', False):
426+
return int(response)
427+
if options.get('idx', False):
428+
if options.get('withmatchlen', False):
429+
matches = [[(int(match[-1]))] + list(map(tuple, match[:-1]))
430+
for match in response[1]]
431+
else:
432+
matches = [list(map(tuple, match))
433+
for match in response[1]]
434+
return {
435+
str_if_bytes(response[0]): matches,
436+
str_if_bytes(response[2]): int(response[3])
437+
}
438+
return str_if_bytes(response)
439+
440+
413441
def parse_cluster_info(response, **options):
414442
response = str_if_bytes(response)
415443
return dict(line.split(':') for line in response.splitlines() if line)
@@ -673,6 +701,7 @@ class Redis(Commands, object):
673701
'MODULE LIST': lambda r: [pairs_to_dict(m) for m in r],
674702
'OBJECT': parse_object,
675703
'PING': lambda r: str_if_bytes(r) == 'PONG',
704+
'STRALGO': parse_stralgo,
676705
'PUBSUB NUMSUB': parse_pubsub_numsub,
677706
'RANDOMKEY': lambda r: r and r or None,
678707
'SCAN': parse_scan,

redis/commands.py

+47
Original file line numberDiff line numberDiff line change
@@ -1100,6 +1100,53 @@ def setrange(self, name, offset, value):
11001100
"""
11011101
return self.execute_command('SETRANGE', name, offset, value)
11021102

1103+
def stralgo(self, algo, value1, value2, specific_argument='strings',
1104+
len=False, idx=False, minmatchlen=None, withmatchlen=False):
1105+
"""
1106+
Implements complex algorithms that operate on strings.
1107+
Right now the only algorithm implemented is the LCS algorithm
1108+
(longest common substring). However new algorithms could be
1109+
implemented in the future.
1110+
1111+
``algo`` Right now must be LCS
1112+
``value1`` and ``value2`` Can be two strings or two keys
1113+
``specific_argument`` Specifying if the arguments to the algorithm
1114+
will be keys or strings. strings is the default.
1115+
``len`` Returns just the len of the match.
1116+
``idx`` Returns the match positions in each string.
1117+
``minmatchlen`` Restrict the list of matches to the ones of a given
1118+
minimal length. Can be provided only when ``idx`` set to True.
1119+
``withmatchlen`` Returns the matches with the len of the match.
1120+
Can be provided only when ``idx`` set to True.
1121+
"""
1122+
# check validity
1123+
supported_algo = ['LCS']
1124+
if algo not in supported_algo:
1125+
raise DataError("The supported algorithms are: %s"
1126+
% (', '.join(supported_algo)))
1127+
if specific_argument not in ['keys', 'strings']:
1128+
raise DataError("specific_argument can be only"
1129+
" keys or strings")
1130+
if len and idx:
1131+
raise DataError("len and idx cannot be provided together.")
1132+
1133+
pieces = [algo, specific_argument.upper(), value1, value2]
1134+
if len:
1135+
pieces.append(b'LEN')
1136+
if idx:
1137+
pieces.append(b'IDX')
1138+
try:
1139+
int(minmatchlen)
1140+
pieces.extend([b'MINMATCHLEN', minmatchlen])
1141+
except TypeError:
1142+
pass
1143+
if withmatchlen:
1144+
pieces.append(b'WITHMATCHLEN')
1145+
1146+
return self.execute_command('STRALGO', *pieces, len=len, idx=idx,
1147+
minmatchlen=minmatchlen,
1148+
withmatchlen=withmatchlen)
1149+
11031150
def strlen(self, name):
11041151
"Return the number of bytes stored in the value of ``name``"
11051152
return self.execute_command('STRLEN', name)

tests/test_commands.py

+43
Original file line numberDiff line numberDiff line change
@@ -1052,6 +1052,49 @@ def test_setrange(self, r):
10521052
assert r.setrange('a', 6, '12345') == 11
10531053
assert r['a'] == b'abcdef12345'
10541054

1055+
@skip_if_server_version_lt('6.0.0')
1056+
def test_stralgo_lcs(self, r):
1057+
key1 = 'key1'
1058+
key2 = 'key2'
1059+
value1 = 'ohmytext'
1060+
value2 = 'mynewtext'
1061+
res = 'mytext'
1062+
# test LCS of strings
1063+
assert r.stralgo('LCS', value1, value2) == res
1064+
# test using keys
1065+
r.mset({key1: value1, key2: value2})
1066+
assert r.stralgo('LCS', key1, key2, specific_argument="keys") == res
1067+
# test other labels
1068+
assert r.stralgo('LCS', value1, value2, len=True) == len(res)
1069+
assert r.stralgo('LCS', value1, value2, idx=True) == \
1070+
{
1071+
'len': len(res),
1072+
'matches': [[(4, 7), (5, 8)], [(2, 3), (0, 1)]]
1073+
}
1074+
assert r.stralgo('LCS', value1, value2,
1075+
idx=True, withmatchlen=True) == \
1076+
{
1077+
'len': len(res),
1078+
'matches': [[4, (4, 7), (5, 8)], [2, (2, 3), (0, 1)]]
1079+
}
1080+
assert r.stralgo('LCS', value1, value2,
1081+
idx=True, minmatchlen=4, withmatchlen=True) == \
1082+
{
1083+
'len': len(res),
1084+
'matches': [[4, (4, 7), (5, 8)]]
1085+
}
1086+
1087+
@skip_if_server_version_lt('6.0.0')
1088+
def test_stralgo_negative(self, r):
1089+
with pytest.raises(exceptions.DataError):
1090+
r.stralgo('ISSUB', 'value1', 'value2')
1091+
with pytest.raises(exceptions.DataError):
1092+
r.stralgo('LCS', 'value1', 'value2', len=True, idx=True)
1093+
with pytest.raises(exceptions.DataError):
1094+
r.stralgo('LCS', 'value1', 'value2', specific_argument="INT")
1095+
with pytest.raises(ValueError):
1096+
r.stralgo('LCS', 'value1', 'value2', idx=True, minmatchlen="one")
1097+
10551098
def test_strlen(self, r):
10561099
r['a'] = 'foo'
10571100
assert r.strlen('a') == 3

0 commit comments

Comments
 (0)