# dialects/postgresql/bitstring.py # Copyright (C) 2013-2026 the SQLAlchemy authors and contributors # # # This module is part of SQLAlchemy and is released under # the MIT License: https://www.opensource.org/licenses/mit-license.php from __future__ import annotations import math from typing import Any from typing import cast from typing import Literal from typing import SupportsIndex class BitString(str): """Represent a PostgreSQL bit string in python. This object is used by the :class:`_postgresql.BIT` type when returning values. :class:`_postgresql.BitString` values may also be constructed directly and used with :class:`_postgresql.BIT` columns:: from sqlalchemy.dialects.postgresql import BitString with engine.connect() as conn: conn.execute(table.insert(), {"data": BitString("011001101")}) .. versionadded:: 2.1 """ _DIGITS = frozenset("01") def __new__(cls, _value: str, _check: bool = True) -> BitString: if isinstance(_value, BitString): return _value elif _check and cls._DIGITS.union(_value) > cls._DIGITS: raise ValueError("BitString must only contain '0' and '1' chars") else: return super().__new__(cls, _value) @classmethod def from_int(cls, value: int, length: int) -> BitString: """Returns a BitString consisting of the bits in the integer ``value``. A ``ValueError`` is raised if ``value`` is not a non-negative integer. If the provided ``value`` can not be represented in a bit string of at most ``length``, a ``ValueError`` will be raised. The bitstring will be padded on the left by ``'0'`` to bits to produce a bitstring of the desired length. """ if value < 0: raise ValueError("value must be non-negative") if length < 0: raise ValueError("length must be non-negative") template_str = f"{{0:0{length}b}}" if length > 0 else "" r = template_str.format(value) if (length == 0 and value > 0) or len(r) > length: raise ValueError( f"Cannot encode {value} as a BitString of length {length}" ) return cls(r) @classmethod def from_bytes(cls, value: bytes, length: int = -1) -> BitString: """Returns a ``BitString`` consisting of the bits in the given ``value`` bytes. If ``length`` is provided, then the length of the provided string will be exactly ``length``, with ``'0'`` bits inserted at the left of the string in order to produce a value of the required length. If the bits obtained by omitting the leading ``'0'`` bits of ``value`` cannot be represented in a string of this length a ``ValueError`` will be raised. """ str_v: str = "".join(f"{int(c):08b}" for c in value) if length >= 0: str_v = str_v.lstrip("0") if len(str_v) > length: raise ValueError( f"Cannot encode {value!r} as a BitString of " f"length {length}" ) str_v = str_v.zfill(length) return cls(str_v) def get_bit(self, index: int) -> Literal["0", "1"]: """Returns the value of the flag at the given index:: BitString("0101").get_flag(4) == "1" """ return cast(Literal["0", "1"], super().__getitem__(index)) @property def bit_length(self) -> int: return len(self) @property def octet_length(self) -> int: return math.ceil(len(self) / 8) def has_bit(self, index: int) -> bool: return self.get_bit(index) == "1" def set_bit( self, index: int, value: bool | int | Literal["0", "1"] ) -> BitString: """Set the bit at index to the given value. If value is an int, then it is considered to be '1' iff nonzero. """ if index < 0 or index >= len(self): raise IndexError("BitString index out of range") if isinstance(value, (bool, int)): value = "1" if value else "0" if self.get_bit(index) == value: return self return BitString( "".join([self[:index], value, self[index + 1 :]]), False ) def lstrip(self, char: str | None = None) -> BitString: """Returns a copy of the BitString with leading characters removed. If omitted or None, 'chars' defaults '0':: BitString("00010101000").lstrip() == BitString("00010101") BitString("11110101111").lstrip("1") == BitString("1111010") """ if char is None: char = "0" return BitString(super().lstrip(char), False) def rstrip(self, char: str | None = "0") -> BitString: """Returns a copy of the BitString with trailing characters removed. If omitted or None, ``'char'`` defaults to "0":: BitString("00010101000").rstrip() == BitString("10101000") BitString("11110101111").rstrip("1") == BitString("10101111") """ if char is None: char = "0" return BitString(super().rstrip(char), False) def strip(self, char: str | None = "0") -> BitString: """Returns a copy of the BitString with both leading and trailing characters removed. If omitted or None, ``'char'`` defaults to ``"0"``:: BitString("00010101000").rstrip() == BitString("10101") BitString("11110101111").rstrip("1") == BitString("1010") """ if char is None: char = "0" return BitString(super().strip(char)) def removeprefix(self, prefix: str, /) -> BitString: return BitString(super().removeprefix(prefix), False) def removesuffix(self, suffix: str, /) -> BitString: return BitString(super().removesuffix(suffix), False) def replace( self, old: str, new: str, count: SupportsIndex = -1, ) -> BitString: new = BitString(new) return BitString(super().replace(old, new, count), False) def split( self, sep: str | None = None, maxsplit: SupportsIndex = -1, ) -> list[str]: return [BitString(word) for word in super().split(sep, maxsplit)] def zfill(self, width: SupportsIndex) -> BitString: return BitString(super().zfill(width), False) def __repr__(self) -> str: return f'BitString("{self.__str__()}")' def __int__(self) -> int: return int(self, 2) if self else 0 def to_bytes(self, length: int = -1) -> bytes: return int(self).to_bytes( length if length >= 0 else self.octet_length, byteorder="big" ) def __bytes__(self) -> bytes: return self.to_bytes() def __getitem__( self, key: SupportsIndex | slice[Any, Any, Any] ) -> BitString: return BitString(super().__getitem__(key), False) def __add__(self, o: str) -> BitString: """Return self + o""" if not isinstance(o, str): raise TypeError( f"Can only concatenate str (not '{type(self)}') to BitString" ) return BitString("".join([self, o])) def __radd__(self, o: str) -> BitString: if not isinstance(o, str): raise TypeError( f"Can only concatenate str (not '{type(self)}') to BitString" ) return BitString("".join([o, self])) def __lshift__(self, amount: int) -> BitString: """Shifts each the bitstring to the left by the given amount. String length is preserved:: BitString("000101") << 1 == BitString("001010") """ return BitString( "".join([self, *("0" for _ in range(amount))])[-len(self) :], False ) def __rshift__(self, amount: int) -> BitString: """Shifts each bit in the bitstring to the right by the given amount. String length is preserved:: BitString("101") >> 1 == BitString("010") """ return BitString(self[:-amount], False).zfill(width=len(self)) def __invert__(self) -> BitString: """Inverts (~) each bit in the bitstring:: ~BitString("01010") == BitString("10101") """ return BitString("".join("1" if x == "0" else "0" for x in self)) def __and__(self, o: str) -> BitString: """Performs a bitwise and (``&``) with the given operand. A ``ValueError`` is raised if the operand is not the same length. e.g.:: BitString("011") & BitString("011") == BitString("010") """ if not isinstance(o, str): return NotImplemented o = BitString(o) if len(self) != len(o): raise ValueError("Operands must be the same length") return BitString( "".join( "1" if (x == "1" and y == "1") else "0" for x, y in zip(self, o) ), False, ) def __or__(self, o: str) -> BitString: """Performs a bitwise or (``|``) with the given operand. A ``ValueError`` is raised if the operand is not the same length. e.g.:: BitString("011") | BitString("010") == BitString("011") """ if not isinstance(o, str): return NotImplemented if len(self) != len(o): raise ValueError("Operands must be the same length") o = BitString(o) return BitString( "".join( "1" if (x == "1" or y == "1") else "0" for (x, y) in zip(self, o) ), False, ) def __xor__(self, o: str) -> BitString: """Performs a bitwise xor (``^``) with the given operand. A ``ValueError`` is raised if the operand is not the same length. e.g.:: BitString("011") ^ BitString("010") == BitString("001") """ if not isinstance(o, BitString): return NotImplemented if len(self) != len(o): raise ValueError("Operands must be the same length") return BitString( "".join( ( "1" if ((x == "1" and y == "0") or (x == "0" and y == "1")) else "0" ) for (x, y) in zip(self, o) ), False, ) __rand__ = __and__ __ror__ = __or__ __rxor__ = __xor__