Skip to content

Commit ffd134e

Browse files
percontationelprans
authored andcommitted
Add BitString.to_int and BitString.from_int
These methods assist in converting between BitStrings and ints, which is useful when bridging between Postgres and things like bitmasks, enum.IntFlag, etc. Their interface is modeled after Python's int.to_bytes and .from_bytes.
1 parent eaa2fa1 commit ffd134e

File tree

2 files changed

+101
-0
lines changed

2 files changed

+101
-0
lines changed

asyncpg/types.py

+87
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,93 @@ def as_string(self):
228228

229229
return s.strip()
230230

231+
def to_int(self, bitorder='big', *, signed=False):
232+
"""Interpret the BitString as a Python int.
233+
Acts similarly to int.from_bytes.
234+
235+
:param bitorder:
236+
Determines the bit order used to interpret the BitString. By
237+
default, this function uses Postgres conventions for casting bits
238+
to ints. If bitorder is 'big', the most significant bit is at the
239+
start of the string (this is the same as the default). If bitorder
240+
is 'little', the most significant bit is at the end of the string.
241+
242+
:param bool signed:
243+
Determines whether two's complement is used to interpret the
244+
BitString. If signed is False, the returned value is always
245+
non-negative.
246+
247+
:return int: An integer representing the BitString. Information about
248+
the BitString's exact length is lost.
249+
250+
.. versionadded:: 0.18.0
251+
"""
252+
x = int.from_bytes(self._bytes, byteorder='big')
253+
x >>= -self._bitlength % 8
254+
if bitorder == 'big':
255+
pass
256+
elif bitorder == 'little':
257+
x = int(bin(x)[:1:-1].ljust(self._bitlength, '0'), 2)
258+
else:
259+
raise ValueError("bitorder must be either 'big' or 'little'")
260+
261+
if signed and self._bitlength > 0 and x & (1 << (self._bitlength - 1)):
262+
x -= 1 << self._bitlength
263+
return x
264+
265+
@classmethod
266+
def from_int(cls, x, length, bitorder='big', *, signed=False):
267+
"""Represent the Python int x as a BitString.
268+
Acts similarly to int.to_bytes.
269+
270+
:param int x:
271+
An integer to represent. Negative integers are represented in two's
272+
complement form, unless the argument signed is False, in which case
273+
negative integers raise an OverflowError.
274+
275+
:param int length:
276+
The length of the resulting BitString. An OverflowError is raised
277+
if the integer is not representable in this many bits.
278+
279+
:param bitorder:
280+
Determines the bit order used in the BitString representation. By
281+
default, this function uses Postgres conventions for casting ints
282+
to bits. If bitorder is 'big', the most significant bit is at the
283+
start of the string (this is the same as the default). If bitorder
284+
is 'little', the most significant bit is at the end of the string.
285+
286+
:param bool signed:
287+
Determines whether two's complement is used in the BitString
288+
representation. If signed is False and a negative integer is given,
289+
an OverflowError is raised.
290+
291+
:return BitString: A BitString representing the input integer, in the
292+
form specified by the other input args.
293+
294+
.. versionadded:: 0.18.0
295+
"""
296+
# Exception types are by analogy to int.to_bytes
297+
if length < 0:
298+
raise ValueError("length argument must be non-negative")
299+
elif length < x.bit_length():
300+
raise OverflowError("int too big to convert")
301+
302+
if x < 0:
303+
if not signed:
304+
raise OverflowError("can't convert negative int to unsigned")
305+
x &= (1 << length) - 1
306+
307+
if bitorder == 'big':
308+
pass
309+
elif bitorder == 'little':
310+
x = int(bin(x)[:1:-1].ljust(length, '0'), 2)
311+
else:
312+
raise ValueError("bitorder must be either 'big' or 'little'")
313+
314+
x <<= (-length % 8)
315+
bytes_ = x.to_bytes((length + 7) // 8, byteorder='big')
316+
return cls.frombytes(bytes_, length)
317+
231318
def __repr__(self):
232319
return '<BitString {}>'.format(self.as_string())
233320

tests/test_codecs.py

+14
Original file line numberDiff line numberDiff line change
@@ -513,6 +513,20 @@ def test_bitstring(self):
513513

514514
self.assertEqual(len(bits.bytes), expected_bytelen)
515515

516+
little, big = bits.to_int('little'), bits.to_int('big')
517+
self.assertEqual(bits.from_int(little, len(bits), 'little'), bits)
518+
self.assertEqual(bits.from_int(big, len(bits), 'big'), bits)
519+
520+
naive_little = 0
521+
for i, c in enumerate(sanitized_bs):
522+
naive_little |= int(c) << i
523+
naive_big = 0
524+
for c in sanitized_bs:
525+
naive_big = (naive_big << 1) | int(c)
526+
527+
self.assertEqual(little, naive_little)
528+
self.assertEqual(big, naive_big)
529+
516530
async def test_interval(self):
517531
res = await self.con.fetchval("SELECT '5 years'::interval")
518532
self.assertEqual(res, datetime.timedelta(days=1825))

0 commit comments

Comments
 (0)