Files
Federico Caselli baec9a3ac8 Add new str` subclass for postgresql bitstring
Adds a new ``str`` subclass :class:`dialects.postgresql.BitString`
representing PostgreSQL bitstrings in python, that includes
functionality for converting to and from ``int`` and ``bytes``, in
addition to implementing utility methods and operators for dealing
with bits.

This new class is returned automatically by the :class:`postgresql.BIT`
type.

Fixes: #10556
Closes: #12594
Pull-request: https://github.com/sqlalchemy/sqlalchemy/pull/12594
Pull-request-sha: da47f40739

Change-Id: I64685660527c23666f7351b2c393fa86dfb643ea
2025-07-01 22:44:41 -04:00

166 lines
5.8 KiB
Python

from sqlalchemy import testing
from sqlalchemy.dialects.postgresql import BitString
from sqlalchemy.testing import fixtures
from sqlalchemy.testing.assertions import assert_raises
from sqlalchemy.testing.assertions import eq_
from sqlalchemy.testing.assertions import is_
from sqlalchemy.testing.assertions import is_false
from sqlalchemy.testing.assertions import is_not
from sqlalchemy.testing.assertions import is_true
class BitStringTests(fixtures.TestBase):
@testing.combinations(
lambda: BitString("111") == BitString("111"),
lambda: BitString("111") == "111",
lambda: BitString("111") != BitString("110"),
lambda: BitString("111") != "110",
lambda: hash(BitString("011")) == hash(BitString("011")),
lambda: hash(BitString("011")) == hash("011"),
lambda: BitString("011")[1] == BitString("1"),
lambda: BitString("010") > BitString("001"),
lambda: "010" > BitString("001"),
lambda: "011" <= BitString("011"),
lambda: "011" <= BitString("101"),
)
def test_comparisons(self, case):
is_true(case())
def test_sorting(self):
eq_(
sorted([BitString("110"), BitString("010"), "111", "101"]),
[BitString("010"), "101", BitString("110"), "111"],
)
def test_str_conversion(self):
x = BitString("1110111")
eq_(str(x), "1110111")
assert_raises(ValueError, lambda: BitString("1246"))
def test_same_instance_returned(self):
x = BitString("1110111")
y = BitString("1110111")
z = BitString(x)
eq_(x, y)
eq_(x, z)
is_not(x, y)
is_(x, z)
@testing.combinations(
(0, 0, BitString("")),
(0, 1, BitString("0")),
(1, 1, BitString("1")),
(1, 0, ValueError),
(1, -1, ValueError),
(2, 1, ValueError),
(-1, 4, ValueError),
(1, 4, BitString("0001")),
(1, 10, BitString("0000000001")),
(127, 8, BitString("01111111")),
(127, 10, BitString("0001111111")),
(1404, 8, ValueError),
(1404, 12, BitString("010101111100")),
argnames="source, bitlen, result_or_error",
)
def test_int_conversion(self, source, bitlen, result_or_error):
if isinstance(result_or_error, type):
assert_raises(
result_or_error, lambda: BitString.from_int(source, bitlen)
)
return
result = result_or_error
bits = BitString.from_int(source, bitlen)
eq_(bits, result)
eq_(int(bits), source)
@testing.combinations(
(b"", -1, BitString("")),
(b"", 4, BitString("0000")),
(b"\x00", 1, BitString("0")),
(b"\x01", 1, BitString("1")),
(b"\x01", 4, BitString("0001")),
(b"\x01", 10, BitString("0000000001")),
(b"\x01", -1, BitString("00000001")),
(b"\xff", 10, BitString("0011111111")),
(b"\xaf\x04", 8, ValueError),
(b"\xaf\x04", 16, BitString("1010111100000100")),
(b"\xaf\x04", 20, BitString("00001010111100000100")),
argnames="source, bitlen, result_or_error",
)
def test_bytes_conversion(self, source, bitlen, result_or_error):
if isinstance(result_or_error, type):
assert_raises(
result_or_error,
lambda: BitString.from_bytes(source, length=bitlen),
)
return
result = result_or_error
bits = BitString.from_bytes(source, bitlen)
eq_(bits, result)
# Expecting a roundtrip conversion in this case is nonsensical
if source == b"" and bitlen > 0:
return
eq_(bits.to_bytes(len(source)), source)
def test_get_set_bit(self):
eq_(BitString("1010").get_bit(2), "1")
eq_(BitString("0101").get_bit(2), "0")
assert_raises(IndexError, lambda: BitString("0").get_bit(1))
eq_(BitString("0101").set_bit(3, "0"), BitString("0100"))
eq_(BitString("0101").set_bit(3, "1"), BitString("0101"))
assert_raises(IndexError, lambda: BitString("1111").set_bit(5, "1"))
def test_string_methods(self):
eq_(BitString("01100").lstrip(), BitString("1100"))
eq_(BitString("01100").rstrip(), BitString("011"))
eq_(BitString("01100").strip(), BitString("11"))
eq_(BitString("11100").removeprefix("111"), BitString("00"))
eq_(BitString("11100").removeprefix("0"), BitString("11100"))
eq_(BitString("11100").removesuffix("10"), BitString("11100"))
eq_(BitString("11100").removesuffix("00"), BitString("111"))
eq_(
BitString("010101011").replace("0101", "11", 1),
BitString("1101011"),
)
eq_(
BitString("01101101").split("1", 2),
[BitString("0"), BitString(""), BitString("01101")],
)
eq_(BitString("0110").split("11"), [BitString("0"), BitString("0")])
eq_(BitString("111").zfill(8), BitString("00000111"))
def test_string_operators(self):
is_true("1" in BitString("001"))
is_true("0" in BitString("110"))
is_false("1" in BitString("000"))
is_true("001" in BitString("01001"))
is_true(BitString("001") in BitString("01001"))
is_false(BitString("000") in BitString("01001"))
eq_(BitString("010") + "001", BitString("010001"))
eq_("001" + BitString("010"), BitString("001010"))
def test_bitwise_operators(self):
eq_(~BitString("0101"), BitString("1010"))
eq_(BitString("010") & BitString("011"), BitString("010"))
eq_(BitString("010") | BitString("011"), BitString("011"))
eq_(BitString("010") ^ BitString("011"), BitString("001"))
eq_(BitString("001100") << 2, BitString("110000"))
eq_(BitString("001100") >> 2, BitString("000011"))