Skip to content

Commit 001c23b

Browse files
authored
Implement BITOP (#110)
close #110
1 parent b3da7d0 commit 001c23b

File tree

4 files changed

+69
-8
lines changed

4 files changed

+69
-8
lines changed

REDIS_COMMANDS.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,8 @@ list of [unimplemented commands](#unimplemented-commands).
6868
### bitmap
6969
* [BITCOUNT](https://redis.io/commands/bitcount/)
7070
Count set bits in a string
71+
* [BITOP](https://redis.io/commands/bitop/)
72+
Perform bitwise operations between strings
7173
* [GETBIT](https://redis.io/commands/getbit/)
7274
Returns the bit value at offset in the string value stored at key
7375
* [SETBIT](https://redis.io/commands/setbit/)
@@ -613,8 +615,6 @@ All the redis commands are implemented in fakeredis with these exceptions:
613615
Perform arbitrary bitfield integer operations on strings
614616
* [BITFIELD_RO](https://redis.io/commands/bitfield_ro/)
615617
Perform arbitrary bitfield integer operations on strings. Read-only variant of BITFIELD
616-
* [BITOP](https://redis.io/commands/bitop/)
617-
Perform bitwise operations between strings
618618
* [BITPOS](https://redis.io/commands/bitpos/)
619619
Find first bit set or clear in a string
620620

fakeredis/_msgs.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
INVALID_OFFSET_MSG = "ERR offset is out of range"
77
INVALID_BIT_OFFSET_MSG = "ERR bit offset is not an integer or out of range"
88
INVALID_BIT_VALUE_MSG = "ERR bit is not an integer or out of range"
9+
BITOP_NOT_ONE_KEY_ONLY = "ERR BITOP NOT must be called with a single source key"
910
INVALID_DB_MSG = "ERR DB index is out of range"
1011
INVALID_MIN_MAX_FLOAT_MSG = "ERR min or max is not a float"
1112
INVALID_MIN_MAX_STR_MSG = "ERR min or max not a valid string range item"

fakeredis/commands_mixins/bitmap_mixin.py

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
class BitmapCommandsMixin:
77
# BITMAP commands
8-
# TODO: bitfield, bitfield_ro, bitop, bitpos
8+
# TODO: bitfield, bitfield_ro, bitpos
99

1010
@command((Key(bytes, 0),), (bytes,))
1111
def bitcount(self, key, *args):
@@ -70,3 +70,38 @@ def setbit(self, key, offset, value):
7070
reconstructed[byte] = new_byte
7171
key.update(bytes(reconstructed))
7272
return old_value
73+
74+
@staticmethod
75+
def _bitop(op, *keys):
76+
value = keys[0].value
77+
if not isinstance(value, bytes):
78+
raise SimpleError(msgs.WRONGTYPE_MSG)
79+
ans = keys[0].value
80+
i = 1
81+
while i < len(keys):
82+
value = keys[i].value if keys[i].value is not None else b''
83+
if not isinstance(value, bytes):
84+
raise SimpleError(msgs.WRONGTYPE_MSG)
85+
ans = bytes(op(a, b) for a, b in zip(ans, value))
86+
i += 1
87+
return ans
88+
89+
@command((bytes, Key(), Key(bytes)), (Key(bytes),))
90+
def bitop(self, op_name, dst, *keys):
91+
if len(keys) == 0:
92+
raise SimpleError(msgs.WRONG_ARGS_MSG6.format('bitop'))
93+
if casematch(op_name, b'and'):
94+
res = self._bitop(lambda a, b: a & b, *keys)
95+
elif casematch(op_name, b'or'):
96+
res = self._bitop(lambda a, b: a | b, *keys)
97+
elif casematch(op_name, b'xor'):
98+
res = self._bitop(lambda a, b: a ^ b, *keys)
99+
elif casematch(op_name, b'not'):
100+
if len(keys) != 1:
101+
raise SimpleError(msgs.BITOP_NOT_ONE_KEY_ONLY)
102+
val = keys[0].value
103+
res = bytes([((1 << 8) - 1 - val[i]) for i in range(len(val))])
104+
else:
105+
raise SimpleError(msgs.WRONG_ARGS_MSG6.format('bitop'))
106+
dst.value = res
107+
return len(dst.value)

test/test_mixins/test_bitmap_commands.py

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -129,8 +129,33 @@ def test_bitcount_wrong_type(r):
129129
with pytest.raises(redis.ResponseError):
130130
r.bitcount('foo')
131131

132-
# def test_bitop(r):
133-
# r.set('key1', 'foobar')
134-
# r.set('key2', 'abcdef')
135-
# assert r.bitop('and', 'dest', 'key1', 'key2') == 6
136-
# assert r.get('dest') == b'`bc`ab'
132+
133+
def test_bitop(r):
134+
r.set('key1', 'foobar')
135+
r.set('key2', 'abcdef')
136+
137+
assert r.bitop('and', 'dest', 'key1', 'key2') == 6
138+
assert r.get('dest') == b'`bc`ab'
139+
140+
assert r.bitop('not', 'dest1', 'key1') == 6
141+
assert r.get('dest1') == b'\x99\x90\x90\x9d\x9e\x8d'
142+
143+
assert r.bitop('or', 'dest-or', 'key1', 'key2') == 6
144+
assert r.get('dest-or') == b'goofev'
145+
146+
assert r.bitop('xor', 'dest-xor', 'key1', 'key2') == 6
147+
assert r.get('dest-xor') == b'\x07\r\x0c\x06\x04\x14'
148+
149+
150+
def test_bitop_errors(r):
151+
r.set('key1', 'foobar')
152+
r.set('key2', 'abcdef')
153+
r.sadd('key-set', 'member1')
154+
with pytest.raises(redis.ResponseError):
155+
r.bitop('not', 'dest', 'key1', 'key2')
156+
with pytest.raises(redis.ResponseError):
157+
r.bitop('badop', 'dest', 'key1', 'key2')
158+
with pytest.raises(redis.ResponseError):
159+
r.bitop('and', 'dest', 'key1', 'key-set')
160+
with pytest.raises(redis.ResponseError):
161+
r.bitop('and', 'dest')

0 commit comments

Comments
 (0)