From d21b56c5de4dbaf5a66c1c7371a2699b56bb7bf7 Mon Sep 17 00:00:00 2001 From: Scott Griffiths Date: Wed, 11 Mar 2026 18:57:51 +0000 Subject: [PATCH 01/35] Starting on v5.0 * Removing bitarray dependency. * Removing tobitarray() method and other compatibilities. * Removing options.using_rust_core flag. * Removing bitarray tests from CI. --- .github/workflows/ci.yml | 95 ------- bitstring/__init__.py | 20 +- bitstring/bitarray_.py | 1 - bitstring/bits.py | 5 - bitstring/bitstore_bitarray.py | 328 ------------------------- bitstring/bitstore_bitarray_helpers.py | 80 ------ bitstring/bitstore_tibs.py | 14 -- bitstring/bitstream.py | 2 - bitstring/bitstring_options.py | 5 - doc/bits.rst | 10 - doc/functions.rst | 24 -- doc/quick_reference.rst | 1 - pyproject.toml | 3 +- tests/test_bitarray.py | 66 ----- tests/test_bits.py | 20 -- tests/test_bitstring.py | 8 - 16 files changed, 5 insertions(+), 677 deletions(-) delete mode 100644 bitstring/bitstore_bitarray.py delete mode 100644 bitstring/bitstore_bitarray_helpers.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 65692a3e..b1217a3d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -160,100 +160,6 @@ jobs: ls -la - name: Run pytest - env: - BITSTRING_USE_RUST_CORE: ${{ matrix.rust.env_var }} - run: | - python -m pytest tests/ --benchmark-disable - - test-rust: - name: Test ${{ matrix.os.name }} ${{ matrix.python.name }} (Rust core) - if: always() - needs: - - test-standard - runs-on: ${{ matrix.os.runs-on }} - strategy: - fail-fast: false - matrix: - os: - - name: 🐧 - runs-on: ubuntu-latest - - name: 🍎 - runs-on: macos-15 - - name: 🪟 - runs-on: windows-latest - python: - - name: CPython 3.8 - major_dot_minor: '3.8' - action: '3.8' - - name: CPython 3.9 - major_dot_minor: '3.9' - action: '3.9' - - name: CPython 3.10 - major_dot_minor: '3.10' - action: '3.10' - - name: CPython 3.11 - major_dot_minor: '3.11' - action: '3.11' - - name: CPython 3.12 - major_dot_minor: '3.12' - action: '3.12' - - name: CPython 3.13 - major_dot_minor: '3.13' - action: '3.13' - - name: CPython 3.14 - major_dot_minor: '3.14' - action: '3.14' - exclude: - - os: - name: "🍎" - runs-on: macos-15 - python: - name: "CPython 3.8" - major_dot_minor: '3.8' - action: '3.8' - - os: - name: "🍎" - runs-on: macos-15 - python: - name: "CPython 3.9" - major_dot_minor: '3.9' - action: '3.9' - - os: - name: "🍎" - runs-on: macos-15 - python: - name: "CPython 3.10" - major_dot_minor: '3.10' - action: '3.10' - steps: - - uses: actions/checkout@v4 - with: - path: repo - - - name: Download package files - uses: actions/download-artifact@v4 - with: - name: packages - path: dist - - - uses: actions/setup-python@v5 - with: - python-version: ${{ fromJSON(format('["{0}", "{1}"]', format('{0}.0-alpha - {0}.X', matrix.python.action), matrix.python.action))[startsWith(matrix.python.action, 'pypy')] }} - architecture: x64 - - - name: Setup environment - run: | - python --version --version - # make sure we test the installed code - cp -R repo/tests/ tests/ - python -m pip install -r tests/requirements.txt - python -m pip install ./dist/*.whl - # show the directory contents for diagnostics - ls -la - - - name: Run pytest - env: - BITSTRING_USE_RUST_CORE: '1' run: | python -m pytest tests/ --benchmark-disable @@ -268,7 +174,6 @@ jobs: # a merge. - build - test-standard - - test-rust steps: - name: Require all successes uses: re-actors/alls-green@v1.2.2 diff --git a/bitstring/__init__.py b/bitstring/__init__.py index 438539b0..46052b00 100644 --- a/bitstring/__init__.py +++ b/bitstring/__init__.py @@ -55,26 +55,14 @@ THE SOFTWARE. """ -__version__ = "4.4.0" +__version__ = "5.0.0" __author__ = "Scott Griffiths" import sys -import os -import importlib - -# New ability to use tibs for core operations instead of bitarray. -# Tibs is written in Rust and is still in beta. Use the environment variable -# BITSTRING_USE_RUST_CORE=1 before importing the module to turn it on. -_env_core = os.getenv('BITSTRING_USE_RUST_CORE', '').strip().lower() -_USE_RUST_CORE = _env_core in ('1', 'true', 'yes', 'on') -if _USE_RUST_CORE: - bitstore = importlib.import_module('bitstring.bitstore_tibs') - bitstore_helpers = importlib.import_module('bitstring.bitstore_tibs_helpers') -else: - bitstore = importlib.import_module('bitstring.bitstore_bitarray') - bitstore_helpers = importlib.import_module('bitstring.bitstore_bitarray_helpers') -bitstore_common_helpers = importlib.import_module('bitstring.bitstore_common_helpers') +import bitstring.bitstore_tibs as bitstore +import bitstring.bitstore_tibs_helpers as bitstore_helpers +import bitstring.bitstore_common_helpers from .bits import Bits from .bitstring_options import Options diff --git a/bitstring/bitarray_.py b/bitstring/bitarray_.py index a372b447..929bcb3f 100644 --- a/bitstring/bitarray_.py +++ b/bitstring/bitarray_.py @@ -52,7 +52,6 @@ class BitArray(Bits): rfind() -- Seek backwards to find a sub-bitstring. split() -- Create generator of chunks split by a delimiter. startswith() -- Return whether the bitstring starts with a sub-bitstring. - tobitarray() -- Return bitstring as a bitarray from the bitarray package. tobytes() -- Return bitstring as bytes, padding if needed. tofile() -- Write bitstring to file, padding if needed. unpack() -- Interpret bits using format string. diff --git a/bitstring/bits.py b/bitstring/bits.py index 5ef096a1..26c02798 100644 --- a/bitstring/bits.py +++ b/bitstring/bits.py @@ -54,7 +54,6 @@ class Bits: rfind() -- Seek backwards to find a sub-bitstring. split() -- Create generator of chunks split by a delimiter. startswith() -- Return whether the bitstring starts with a sub-bitstring. - tobitarray() -- Return bitstring as a bitarray from the bitarray package. tobytes() -- Return bitstring as bytes, padding if needed. tofile() -- Write bitstring to file, padding if needed. unpack() -- Interpret bits using format string. @@ -1451,10 +1450,6 @@ def tobytes(self) -> bytes: """ return self._bitstore.to_bytes() - def tobitarray(self) -> bitarray.bitarray: - """Convert the bitstring to a bitarray object.""" - return self._bitstore.tobitarray() - def tofile(self, f: BinaryIO) -> None: """Write the bitstring to a file object, padding with zero bits if needed. diff --git a/bitstring/bitstore_bitarray.py b/bitstring/bitstore_bitarray.py deleted file mode 100644 index 80e9d1a4..00000000 --- a/bitstring/bitstore_bitarray.py +++ /dev/null @@ -1,328 +0,0 @@ -from __future__ import annotations - -import bitarray -import bitarray.util -from bitstring.exceptions import CreationError -from typing import Union, Iterable, Optional, overload, Iterator, Any -from bitstring.helpers import offset_slice_indices_lsb0 - -if bitarray.__version__.startswith("2."): - raise ImportError(f"bitstring version 4.3 requires bitarray version 3 or higher. Found version {bitarray.__version__}.") - - -class _BitStore: - """A light wrapper around bitarray that does the LSB0 stuff""" - - __slots__ = ('_bitarray', 'modified_length', 'immutable') - - def __init__(self, initializer: Union[bitarray.bitarray, None] = None, - immutable: bool = False) -> None: - if isinstance(initializer, str): - assert False - self._bitarray = bitarray.bitarray(initializer) - self.immutable = immutable - self.modified_length = None - - @classmethod - def from_zeros(cls, i: int) -> _BitStore: - x = super().__new__(cls) - x._bitarray = bitarray.bitarray(i) - x.immutable = False - x.modified_length = None - return x - - - @classmethod - def from_bin(cls, s: str) -> _BitStore: - x = super().__new__(cls) - x._bitarray = bitarray.bitarray(s) - x.immutable = False - x.modified_length = None - return x - - @classmethod - def from_bytes(cls, b: Union[bytes, bytearray, memoryview], /) -> _BitStore: - x = super().__new__(cls) - x._bitarray = bitarray.bitarray() - x._bitarray.frombytes(b) - x.immutable = False - x.modified_length = None - return x - - @classmethod - def frombuffer(cls, buffer, /, length: Optional[int] = None) -> _BitStore: - x = super().__new__(cls) - x._bitarray = bitarray.bitarray(buffer=buffer) - x.immutable = True - x.modified_length = length - # Here 'modified' means it shouldn't be changed further, so setting, deleting etc. are disallowed. - if x.modified_length is not None: - if x.modified_length < 0: - raise CreationError("Can't create bitstring with a negative length.") - if x.modified_length > len(x._bitarray): - raise CreationError( - f"Can't create bitstring with a length of {x.modified_length} from {len(x._bitarray)} bits of data.") - return x - - @classmethod - def join(cls, bitstores: Iterable[_BitStore], /) -> _BitStore: - x = super().__new__(cls) - x._bitarray = bitarray.bitarray() - for b in bitstores: - x._bitarray += b._bitarray - x.immutable = False - x.modified_length = None - return x - - @staticmethod - def using_rust_core() -> bool: - return False - - def tobitarray(self) -> bitarray.bitarray: - if self.modified_length is not None: - return self.getslice(0, len(self))._bitarray - return self._bitarray - - def to_bytes(self) -> bytes: - if self.modified_length is not None: - return self._bitarray[:self.modified_length].tobytes() - return self._bitarray.tobytes() - - def to_u(self) -> int: - if self.modified_length is not None: - return bitarray.util.ba2int(self._bitarray[:self.modified_length], signed=False) - return bitarray.util.ba2int(self._bitarray, signed=False) - - def to_i(self) -> int: - if self.modified_length is not None: - return bitarray.util.ba2int(self._bitarray[:self.modified_length], signed=True) - return bitarray.util.ba2int(self._bitarray, signed=True) - - def to_hex(self) -> str: - if self.modified_length is not None: - return bitarray.util.ba2hex(self._bitarray[:self.modified_length]) - return bitarray.util.ba2hex(self._bitarray) - - def to_bin(self) -> str: - if self.modified_length is not None: - return self._bitarray[:self.modified_length].to01() - return self._bitarray.to01() - - def to_oct(self) -> str: - if self.modified_length is not None: - return bitarray.util.ba2base(8, self._bitarray[:self.modified_length]) - return bitarray.util.ba2base(8, self._bitarray) - - def __imul__(self, n: int, /) -> _BitStore: - self._bitarray *= n - return self - - def __ilshift__(self, n: int, /) -> None: - self._bitarray <<= n - - def __irshift__(self, n: int, /) -> None: - self._bitarray >>= n - - def __iadd__(self, other: _BitStore, /) -> _BitStore: - self._bitarray += other._bitarray - return self - - def __add__(self, other: _BitStore, /) -> _BitStore: - bs = self._mutable_copy() - bs += other - return bs - - def __eq__(self, other: Any, /) -> bool: - return self._bitarray == other._bitarray - - def __and__(self, other: _BitStore, /) -> _BitStore: - return _BitStore(self._bitarray & other._bitarray) - - def __or__(self, other: _BitStore, /) -> _BitStore: - return _BitStore(self._bitarray | other._bitarray) - - def __xor__(self, other: _BitStore, /) -> _BitStore: - return _BitStore(self._bitarray ^ other._bitarray) - - def __iand__(self, other: _BitStore, /) -> _BitStore: - self._bitarray &= other._bitarray - return self - - def __ior__(self, other: _BitStore, /) -> _BitStore: - self._bitarray |= other._bitarray - return self - - def __ixor__(self, other: _BitStore, /) -> _BitStore: - self._bitarray ^= other._bitarray - return self - - def __invert__(self) -> _BitStore: - return _BitStore(~self._bitarray) - - def find(self, bs: _BitStore, start: int, end: int, bytealigned: bool = False) -> int | None: - if not bytealigned: - x = self._bitarray.find(bs._bitarray, start, end) - return None if x == -1 else x - try: - return next(self.findall_msb0(bs, start, end, bytealigned)) - except StopIteration: - return None - - def rfind(self, bs: _BitStore, start: int, end: int, bytealigned: bool = False) -> int | None: - if not bytealigned: - x = self._bitarray.find(bs._bitarray, start, end, right=True) - return None if x == -1 else x - try: - return next(self.rfindall_msb0(bs, start, end, bytealigned)) - except StopIteration: - return None - - def findall_msb0(self, bs: _BitStore, start: int, end: int, bytealigned: bool = False) -> Iterator[int]: - if bytealigned is True and len(bs) % 8 == 0: - # Special case, looking for whole bytes on whole byte boundaries - bytes_ = bs.to_bytes() - # Round up start byte to next byte, and round end byte down. - # We're only looking for whole bytes, so can ignore bits at either end. - start_byte = (start + 7) // 8 - end_byte = end // 8 - b = self._bitarray[start_byte * 8: end_byte * 8].tobytes() - byte_pos = 0 - bytes_to_search = end_byte - start_byte - while byte_pos < bytes_to_search: - byte_pos = b.find(bytes_, byte_pos) - if byte_pos == -1: - break - yield (byte_pos + start_byte) * 8 - byte_pos = byte_pos + 1 - return - # General case - i = self._bitarray.search(bs._bitarray, start, end) - if not bytealigned: - for p in i: - yield p - else: - for p in i: - if (p % 8) == 0: - yield p - - def rfindall_msb0(self, bs: _BitStore, start: int, end: int, bytealigned: bool = False) -> Iterator[int]: - i = self._bitarray.search(bs._bitarray, start, end, right=True) - if not bytealigned: - for p in i: - yield p - else: - for p in i: - if (p % 8) == 0: - yield p - - def count(self, value, /) -> int: - return self._bitarray.count(value) - - def clear(self) -> None: - self._bitarray.clear() - - def reverse(self) -> None: - self._bitarray.reverse() - - def __iter__(self) -> Iterable[bool]: - for i in range(len(self)): - yield self.getindex(i) - - def _mutable_copy(self) -> _BitStore: - """Always creates a copy, even if instance is immutable.""" - return _BitStore(self._bitarray, immutable=False) - - def as_immutable(self) -> _BitStore: - return _BitStore(self._bitarray, immutable=True) - - def copy(self) -> _BitStore: - return self if self.immutable else self._mutable_copy() - - def __getitem__(self, item: Union[int, slice], /) -> Union[int, _BitStore]: - # Use getindex or getslice instead - raise NotImplementedError - - def getindex_msb0(self, index: int, /) -> bool: - return bool(self._bitarray.__getitem__(index)) - - def getslice_withstep_msb0(self, key: slice, /) -> _BitStore: - if self.modified_length is not None: - key = slice(*key.indices(self.modified_length)) - return _BitStore(self._bitarray.__getitem__(key)) - - def getslice_withstep_lsb0(self, key: slice, /) -> _BitStore: - key = offset_slice_indices_lsb0(key, len(self)) - return _BitStore(self._bitarray.__getitem__(key)) - - def getslice_msb0(self, start: Optional[int], stop: Optional[int], /) -> _BitStore: - if self.modified_length is not None: - key = slice(*slice(start, stop, None).indices(self.modified_length)) - start = key.start - stop = key.stop - return _BitStore(self._bitarray[start:stop]) - - def getslice_lsb0(self, start: Optional[int], stop: Optional[int], /) -> _BitStore: - s = offset_slice_indices_lsb0(slice(start, stop, None), len(self)) - return _BitStore(self._bitarray[s.start:s.stop]) - - def getindex_lsb0(self, index: int, /) -> bool: - return bool(self._bitarray.__getitem__(-index - 1)) - - @overload - def setitem_lsb0(self, key: int, value: int, /) -> None: - ... - - @overload - def setitem_lsb0(self, key: slice, value: _BitStore, /) -> None: - ... - - def setitem_lsb0(self, key: Union[int, slice], value: Union[int, _BitStore], /) -> None: - if isinstance(key, slice): - new_slice = offset_slice_indices_lsb0(key, len(self)) - self._bitarray.__setitem__(new_slice, value._bitarray) - else: - self._bitarray.__setitem__(-key - 1, value) - - def delitem_lsb0(self, key: Union[int, slice], /) -> None: - if isinstance(key, slice): - new_slice = offset_slice_indices_lsb0(key, len(self)) - self._bitarray.__delitem__(new_slice) - else: - self._bitarray.__delitem__(-key - 1) - - def invert_msb0(self, index: Optional[int] = None, /) -> None: - if index is not None: - self._bitarray.invert(index) - else: - self._bitarray.invert() - - def invert_lsb0(self, index: Optional[int] = None, /) -> None: - if index is not None: - self._bitarray.invert(-index - 1) - else: - self._bitarray.invert() - - def extend_left(self, other: _BitStore, /) -> None: - self._bitarray = other._bitarray + self._bitarray - - def any(self) -> bool: - return self._bitarray.any() - - def all(self) -> bool: - return self._bitarray.all() - - def __len__(self) -> int: - return self.modified_length if self.modified_length is not None else len(self._bitarray) - - def setitem_msb0(self, key, value, /): - if isinstance(value, _BitStore): - self._bitarray.__setitem__(key, value._bitarray) - else: - self._bitarray.__setitem__(key, value) - - def delitem_msb0(self, key, /): - self._bitarray.__delitem__(key) - - -ConstBitStore = _BitStore -MutableBitStore = _BitStore \ No newline at end of file diff --git a/bitstring/bitstore_bitarray_helpers.py b/bitstring/bitstore_bitarray_helpers.py deleted file mode 100644 index 336957df..00000000 --- a/bitstring/bitstore_bitarray_helpers.py +++ /dev/null @@ -1,80 +0,0 @@ -from __future__ import annotations - -import struct -import math -import functools -from typing import Union, Optional, Dict, Callable -import bitarray -import bitstring -from bitstring.fp8 import p4binary_fmt, p3binary_fmt -from bitstring.mxfp import (e3m2mxfp_fmt, e2m3mxfp_fmt, e2m1mxfp_fmt, e4m3mxfp_saturate_fmt, - e5m2mxfp_saturate_fmt, e4m3mxfp_overflow_fmt, e5m2mxfp_overflow_fmt) -from bitstring.helpers import tidy_input_string - -ConstBitStore = bitstring.bitstore.ConstBitStore -MutableBitStore = bitstring.bitstore.MutableBitStore - - -def bin2bitstore(binstring: str) -> ConstBitStore: - binstring = tidy_input_string(binstring) - binstring = binstring.replace('0b', '') - try: - return ConstBitStore.from_bin(binstring) - except ValueError: - raise bitstring.CreationError(f"Invalid character in bin initialiser {binstring}.") - - -def hex2bitstore(hexstring: str) -> ConstBitStore: - hexstring = tidy_input_string(hexstring) - hexstring = hexstring.replace('0x', '') - try: - ba = bitarray.util.hex2ba(hexstring) - except ValueError: - raise bitstring.CreationError("Invalid symbol in hex initialiser.") - return ConstBitStore(ba) - - -def oct2bitstore(octstring: str) -> ConstBitStore: - octstring = tidy_input_string(octstring) - octstring = octstring.replace('0o', '') - try: - ba = bitarray.util.base2ba(8, octstring) - except ValueError: - raise bitstring.CreationError("Invalid symbol in oct initialiser.") - return ConstBitStore(ba) - - -def int2bitstore(i: int, length: int, signed: bool) -> ConstBitStore: - i = int(i) - try: - x = ConstBitStore(bitarray.util.int2ba(i, length=length, endian='big', signed=signed)) - except OverflowError as e: - if signed: - if i >= (1 << (length - 1)) or i < -(1 << (length - 1)): - raise bitstring.CreationError(f"{i} is too large a signed integer for a bitstring of length {length}. " - f"The allowed range is [{-(1 << (length - 1))}, {(1 << (length - 1)) - 1}].") - else: - if i >= (1 << length): - raise bitstring.CreationError(f"{i} is too large an unsigned integer for a bitstring of length {length}. " - f"The allowed range is [0, {(1 << length) - 1}].") - if i < 0: - raise bitstring.CreationError("uint cannot be initialised with a negative number.") - raise e - return x - - -def intle2bitstore(i: int, length: int, signed: bool) -> ConstBitStore: - x = int2bitstore(i, length, signed).to_bytes() - return ConstBitStore.from_bytes(x[::-1]) - - -def float2bitstore(f: Union[str, float], length: int, big_endian: bool) -> ConstBitStore: - f = float(f) - fmt = {16: '>e', 32: '>f', 64: '>d'}[length] if big_endian else {16: ' 0 else float('-inf')) - return ConstBitStore.from_bytes(b) - diff --git a/bitstring/bitstore_tibs.py b/bitstring/bitstore_tibs.py index f11c42aa..e6547120 100644 --- a/bitstring/bitstore_tibs.py +++ b/bitstring/bitstore_tibs.py @@ -65,13 +65,6 @@ def from_bin(cls, s: str) -> ConstBitStore: def set(self, value, pos) -> None: self._bits.set(value, pos) - @staticmethod - def using_rust_core() -> bool: - return True - - def tobitarray(self): - raise TypeError("tobitarray() is not available when using the Rust core option.") - def to_bytes(self, pad_at_end: bool = True) -> bytes: excess_bits = len(self._bits) % 8 if excess_bits != 0: @@ -246,13 +239,6 @@ def from_bin(cls, s: str) -> MutableBitStore: def set(self, value, pos) -> None: self._bits.set(value, pos) - @staticmethod - def using_rust_core() -> bool: - return True - - def tobitarray(self): - raise TypeError("tobitarray() is not available when using the Rust core option.") - def to_bytes(self, pad_at_end: bool = True) -> bytes: excess_bits = len(self._bits) % 8 if excess_bits != 0: diff --git a/bitstring/bitstream.py b/bitstring/bitstream.py index 3d10bda1..0c001583 100644 --- a/bitstring/bitstream.py +++ b/bitstring/bitstream.py @@ -33,7 +33,6 @@ class ConstBitStream(Bits): rfind() -- Seek backwards to find a sub-bitstring. split() -- Create generator of chunks split by a delimiter. startswith() -- Return whether the bitstring starts with a sub-bitstring. - tobitarray() -- Return bitstring as a bitarray from the bitarray package. tobytes() -- Return bitstring as bytes, padding if needed. tofile() -- Write bitstring to file, padding if needed. unpack() -- Interpret bits using format string. @@ -545,7 +544,6 @@ class BitStream(ConstBitStream, bitstring.BitArray): set() -- Set bit(s) to 1 or 0. split() -- Create generator of chunks split by a delimiter. startswith() -- Return whether the bitstring starts with a sub-bitstring. - tobitarray() -- Return bitstring as a bitarray from the bitarray package. tobytes() -- Return bitstring as bytes, padding if needed. tofile() -- Write bitstring to file, padding if needed. unpack() -- Interpret bits using format string. diff --git a/bitstring/bitstring_options.py b/bitstring/bitstring_options.py index 306ea4fb..e3c8272d 100644 --- a/bitstring/bitstring_options.py +++ b/bitstring/bitstring_options.py @@ -20,11 +20,6 @@ def __init__(self): no_color = os.getenv('NO_COLOR') self.no_color = True if no_color else False - @property - def using_rust_core(self): - x = ConstBitStore() - return x.using_rust_core() - @property def mxfp_overflow(self) -> str: return self._mxfp_overflow diff --git a/doc/bits.rst b/doc/bits.rst index 7b2ba49f..f705a1e4 100644 --- a/doc/bits.rst +++ b/doc/bits.rst @@ -250,16 +250,6 @@ Methods >>> s.startswith('0b111011') True -.. method:: Bits.tobitarray() -> bitarray.bitarray - - Returns the bitstring as a ``bitarray`` object. - - Converts the bitstring to an equivalent ``bitarray`` object from the ``bitarray`` package. - This shouldn't be confused with the ``BitArray`` type provided in the ``bitstring`` package - the ``bitarray`` package is a separate third-party way of representing binary objects. - - Note that ``BitStream`` and ``ConstBitStream`` types that have a bit position do support this method but the bit position information will be lost. - - .. method:: Bits.tobytes() -> bytes diff --git a/doc/functions.rst b/doc/functions.rst index 9ff2d7a3..94b0a764 100644 --- a/doc/functions.rst +++ b/doc/functions.rst @@ -228,30 +228,6 @@ See https://no-color.org for more information. The terminal colours can also be turned off by setting ``bitstring.options.no_color`` to ``True``. -using_rust_core -^^^^^^^^^^^^^^^ - -.. data:: bitstring.using_rust_core : bool - -By default the C-based bitarray library is used to optimise the core operations in bitstring. -This being replaced with the `tibs `_ library, which is written in Rust and by the -author of bitstring. -For now both options are available, and bitarray remains the default. - -The ``using_rust_core`` flag is read-only - to try the new library set the ``BITSTRING_USE_RUST_CORE`` environment variable before running your -program, tests or Python interpreter. :: - - % BITSTRING_USE_RUST_CORE=1 python - Python 3.13.5 (main, Jul 23 2025, 00:30:58) [Clang 20.1.4 ] on darwin - Type "help", "copyright", "credits" or "license" for more information. - >>> import bitstring - >>> bitstring.options.using_rust_core - True - -This should work identically in terms of features, except that the ``tobitarray()`` method will be unavailable. - -The plan is to make the Rust core the only option from version 5 of bitstring. - ---- Command Line Usage diff --git a/doc/quick_reference.rst b/doc/quick_reference.rst index d83520cc..d9cd1702 100644 --- a/doc/quick_reference.rst +++ b/doc/quick_reference.rst @@ -91,7 +91,6 @@ Methods * :meth:`~Bits.rfind` -- Seek backwards to find a sub-bitstring. * :meth:`~Bits.split` -- Create generator of chunks split by a delimiter. * :meth:`~Bits.startswith` -- Return whether the bitstring starts with a sub-bitstring. -* :meth:`~Bits.tobitarray` -- Return bitstring as a ``bitarray`` object from the `bitarray `_ package. * :meth:`~Bits.tobytes` -- Return bitstring as bytes, padding if needed. * :meth:`~Bits.tofile` -- Write bitstring to file, padding if needed. * :meth:`~Bits.unpack` -- Interpret bits using format string. diff --git a/pyproject.toml b/pyproject.toml index 64b27c41..bd2b0deb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "bitstring" -version = "4.4.0" +version = "5.0.0_beta1" license = { text = "MIT" } authors = [ { name="Scott Griffiths", email="dr.scottgriffiths@gmail.com" }, @@ -29,7 +29,6 @@ classifiers = [ ] keywords = ["binary", "bitarray", "bitvector", "bitfield"] dependencies = [ - "bitarray >= 3.0.0, < 4.0", "tibs >= 0.5.6, < 0.6", ] diff --git a/tests/test_bitarray.py b/tests/test_bitarray.py index 2e4d5fa0..cffc4426 100644 --- a/tests/test_bitarray.py +++ b/tests/test_bitarray.py @@ -911,72 +911,6 @@ def test_native_endian_string_initialisers(self): THIS_DIR = os.path.dirname(os.path.abspath(__file__)) -class TestBitarray: - - def teardown_method(self) -> None: - bitstring.lsb0 = False - - def test_to_bitarray(self): - a = BitArray('0xff, 0b0') - if a._bitstore.using_rust_core(): - with pytest.raises(TypeError): - _ = a.tobitarray() - else: - b = a.tobitarray() - assert type(b) == bitarray.bitarray - assert b == bitarray.bitarray('111111110') - - def test_to_bitarray_lsb0(self): - bitstring.lsb0 = True - a = bitstring.Bits('0xff, 0b0') - if a._bitstore.using_rust_core(): - with pytest.raises(TypeError): - _ = a.tobitarray() - else: - b = a.tobitarray() - assert type(b) == bitarray.bitarray - assert b == bitarray.bitarray('111111110') - - def test_from_file(self): - a = bitstring.ConstBitStream(filename=os.path.join(THIS_DIR, 'smalltestfile')) - if a._bitstore.using_rust_core(): - with pytest.raises(TypeError): - _ = a.tobitarray() - else: - b = a.tobitarray() - assert a.bin == b.to01() - - def test_with_offset(self): - a = bitstring.ConstBitStream(filename=os.path.join(THIS_DIR, 'smalltestfile')) - b = bitstring.ConstBitStream(filename=os.path.join(THIS_DIR, 'smalltestfile'), offset=11) - assert len(a) == len(b) + 11 - if a._bitstore.using_rust_core(): - with pytest.raises(TypeError): - _ = a.tobitarray() - else: - assert a[11:].tobitarray() == b.tobitarray() - - def test_with_length(self): - a = bitstring.ConstBitStream(filename=os.path.join(THIS_DIR, 'smalltestfile')) - b = bitstring.ConstBitStream(filename=os.path.join(THIS_DIR, 'smalltestfile'), length=11) - assert len(b) == 11 - if a._bitstore.using_rust_core(): - with pytest.raises(TypeError): - _ = a.tobitarray() - else: - assert a[:11].tobitarray() == b.tobitarray() - - def test_with_offset_and_length(self): - a = bitstring.ConstBitStream(filename=os.path.join(THIS_DIR, 'smalltestfile')) - b = bitstring.ConstBitStream(filename=os.path.join(THIS_DIR, 'smalltestfile'), offset=17, length=7) - assert len(b) == 7 - if a._bitstore.using_rust_core(): - with pytest.raises(TypeError): - _ = a.tobitarray() - else: - assert a[17:24].tobitarray() == b.tobitarray() - - try: import numpy as np numpy_installed = True diff --git a/tests/test_bits.py b/tests/test_bits.py index bd44b966..6a801a0d 100644 --- a/tests/test_bits.py +++ b/tests/test_bits.py @@ -203,26 +203,6 @@ def test_creation_keyword_error(self): with pytest.raises(bitstring.CreationError): Bits(squirrel=5) - @pytest.mark.skipif(bool(os.environ.get('BITSTRING_USE_RUST_CORE')), reason="bitarray not supported with Rust backend") - def test_creation_from_bitarray(self): - ba = bitarray.bitarray('0010') - bs = Bits(ba) - assert bs.bin == '0010' - bs2 = Bits(bitarray=ba) - assert bs2.bin == '0010' - - @pytest.mark.skipif(bool(os.environ.get('BITSTRING_USE_RUST_CORE')), reason="bitarray not supported with Rust backend") - def test_creation_from_frozen_bitarray(self): - fba = bitarray.frozenbitarray('111100001') - ba = Bits(fba) - assert ba.bin == '111100001' - bs2 = Bits(bitarray=fba) - assert bs2.bin == '111100001' - bs3 = Bits(bitarray=fba, offset=4) - assert bs3.bin == '00001' - bs3 = Bits(bitarray=fba, offset=4, length=4) - assert bs3.bin == '0000' - def test_creation_from_bitarray_errors(self): ba = bitarray.bitarray('0101') with pytest.raises(bitstring.CreationError): diff --git a/tests/test_bitstring.py b/tests/test_bitstring.py index 98babebb..be125496 100644 --- a/tests/test_bitstring.py +++ b/tests/test_bitstring.py @@ -198,11 +198,3 @@ def test_reading_float_with_no_length(self): a = bitstring.BitStream(float=14, length=16) b = a.read('float') assert b == 14.0 - -def test_rust_core(): - using_rust = bitstring.options.using_rust_core - x = bitstring.BitStream('0x1') - if hasattr(x._bitstore, "_bitarray"): - assert using_rust is False - else: - assert using_rust is True \ No newline at end of file From 382527566ec364ed355bfd89ea6b4cb9649ae7ea Mon Sep 17 00:00:00 2001 From: Scott Griffiths Date: Wed, 11 Mar 2026 20:08:57 +0000 Subject: [PATCH 02/35] Removing more bitarray - replacing with tibs when needed. --- bitstring/bits.py | 22 ++-------------------- bitstring/fp8.py | 12 ++++++------ bitstring/mxfp.py | 12 ++++++------ doc/introduction.rst | 3 +-- doc/optimisation.rst | 2 +- tests/test_bitarray.py | 1 - tests/test_bits.py | 10 ---------- 7 files changed, 16 insertions(+), 46 deletions(-) diff --git a/bitstring/bits.py b/bitstring/bits.py index 26c02798..156c0d84 100644 --- a/bitstring/bits.py +++ b/bitstring/bits.py @@ -10,7 +10,6 @@ from collections import abc import functools from typing import Tuple, Union, List, Iterable, Any, Optional, BinaryIO, TextIO, overload, Iterator, Type, TypeVar -import bitarray import bitstring from bitstring import utils from bitstring.dtypes import Dtype, dtype_register @@ -25,7 +24,7 @@ # Things that can be converted to Bits when a Bits type is needed -BitsType = Union['Bits', str, Iterable[Any], bool, BinaryIO, bytearray, bytes, memoryview, bitarray.bitarray] +BitsType = Union['Bits', str, Iterable[Any], bool, BinaryIO, bytearray, bytes, memoryview] TBits = TypeVar("TBits", bound='Bits') @@ -153,8 +152,6 @@ def _initialise(self, auto: Any, /, length: Optional[int], offset: Optional[int] self._setbytes_with_truncation(v, length, offset) elif k == 'filename': self._setfile(v, length, offset) - elif k == 'bitarray': - self._setbitarray(v, length, offset) elif k == 'auto': raise bitstring.CreationError( f"The 'auto' parameter should not be given explicitly - just use the first positional argument. " @@ -508,8 +505,6 @@ def _setauto_no_length_or_offset(self, s: BitsType, /) -> None: self._bitstore = ConstBitStore.from_bytes(s.getvalue()) elif isinstance(s, io.BufferedReader): self._setfile(s.name) - elif isinstance(s, bitarray.bitarray): - self._bitstore = ConstBitStore(s) elif isinstance(s, array.array): self._bitstore = ConstBitStore.from_bytes(s.tobytes()) elif isinstance(s, abc.Iterable): @@ -548,7 +543,7 @@ def _setauto(self, s: BitsType, length: Optional[int], offset: Optional[int], /) return if isinstance(s, (str, Bits, bytes, bytearray, memoryview, io.BytesIO, io.BufferedReader, - bitarray.bitarray, array.array, abc.Iterable)): + array.array, abc.Iterable)): raise bitstring.CreationError(f"Cannot initialise bitstring from type '{type(s)}' when using explicit lengths or offsets.") raise TypeError(f"Cannot initialise bitstring from type '{type(s)}'.") @@ -573,19 +568,6 @@ def _setfile(self, filename: str, length: Optional[int] = None, offset: Optional if len(self) != length: raise bitstring.CreationError(f"Can't use a length of {length} bits and an offset of {offset} bits as file length is only {len(temp)} bits.") - def _setbitarray(self, ba: bitarray.bitarray, length: Optional[int], offset: Optional[int]) -> None: - if offset is None: - offset = 0 - if offset > len(ba): - raise bitstring.CreationError(f"Offset of {offset} too large for bitarray of length {len(ba)}.") - if length is None: - self._bitstore = ConstBitStore(ba[offset:]) - else: - if offset + length > len(ba): - raise bitstring.CreationError( - f"Offset of {offset} and length of {length} too large for bitarray of length {len(ba)}.") - self._bitstore = ConstBitStore(ba[offset: offset + length]) - def _setbits(self, bs: BitsType, length: None = None) -> None: bs = Bits._create_from_bitstype(bs) self._bitstore = bs._bitstore diff --git a/bitstring/fp8.py b/bitstring/fp8.py index 575dbbb4..6e857c97 100644 --- a/bitstring/fp8.py +++ b/bitstring/fp8.py @@ -7,9 +7,9 @@ import struct import zlib import array -import bitarray from bitstring.luts import binary8_luts_compressed import math +import tibs class Binary8Format: @@ -66,17 +66,17 @@ def createLUT_for_binary8_to_float(self): """Create a LUT to convert an int in range 0-255 representing a float8 into a Python float""" i2f = [] for i in range(256): - b = bitarray.util.int2ba(i, length=8, endian='big', signed=False) + b = tibs.Tibs.from_u(i, 8) sign = b[0] - exponent = bitarray.util.ba2int(b[1:1 + self.exp_bits]) + exponent = b[1:1 + self.exp_bits].to_u() significand = b[1 + self.exp_bits:] if exponent == 0: - significand = bitarray.bitarray('0') + significand + significand = [0] + significand exponent = -self.bias + 1 else: - significand = bitarray.bitarray('1') + significand + significand = [1] + significand exponent -= self.bias - f = float(bitarray.util.ba2int(significand)) / (2.0 ** (7 - self.exp_bits)) + f = float(significand.to_u()) / (2.0 ** (7 - self.exp_bits)) f *= 2 ** exponent i2f.append(f if not sign else -f) # One special case for minus zero diff --git a/bitstring/mxfp.py b/bitstring/mxfp.py index d675c359..e9acabb7 100644 --- a/bitstring/mxfp.py +++ b/bitstring/mxfp.py @@ -1,7 +1,7 @@ import array import math import struct -import bitarray +import tibs from bitstring.luts import mxfp_luts_compressed import zlib from typing import Optional @@ -127,17 +127,17 @@ def createLUT_for_int_to_float(self) -> array.array: i2f = [] length = 1 + self.exp_bits + self.mantissa_bits for i in range(1 << length): - b = bitarray.util.int2ba(i, length=length, endian='big', signed=False) + b = tibs.Tibs.from_u(i, length) sign = b[0] - exponent = bitarray.util.ba2int(b[1:1 + self.exp_bits]) + exponent = b[1:1 + self.exp_bits].to_u() significand = b[1 + self.exp_bits:] if exponent == 0: - significand = bitarray.bitarray('0') + significand + significand = [0] + significand exponent = -self.bias + 1 else: - significand = bitarray.bitarray('1') + significand + significand = [1] + significand exponent -= self.bias - f = float(bitarray.util.ba2int(significand)) / (2.0 ** self.mantissa_bits) + f = float(significand.to_u()) / (2.0 ** self.mantissa_bits) f *= 2 ** exponent if length == 8: # Some special cases diff --git a/doc/introduction.rst b/doc/introduction.rst index c7c4c5cc..0613718e 100644 --- a/doc/introduction.rst +++ b/doc/introduction.rst @@ -87,7 +87,6 @@ The first parameter when creating a bitstring is a positional only parameter, re * A file object, opened in binary mode, from which the bitstring will be formed. * A ``bytearray`` or ``bytes`` object. * An ``array`` object from the built-in ``array`` module. This is used after being converted to it's constituent byte data via its ``tobytes`` method. -* A ``bitarray`` or ``frozenbitarray`` object from the 3rd party ``bitarray`` package. If it is a string then that string will be parsed into tokens to construct the binary data: @@ -141,7 +140,7 @@ Integers won't be promoted, but instead will raise a ``TypeError``:: ``BitsType`` ^^^^^^^^^^^^ -.. class:: BitsType(Bits | str | Iterable[Any] | bool | BinaryIO | bytearray | bytes | memoryview | bitarray.bitarray) +.. class:: BitsType(Bits | str | Iterable[Any] | bool | BinaryIO | bytearray | bytes | memoryview) The ``BitsType`` type is used in the documentation in a number of places where an object of any type that can be promoted to a bitstring is acceptable. diff --git a/doc/optimisation.rst b/doc/optimisation.rst index 1939607b..3d2a4fb9 100644 --- a/doc/optimisation.rst +++ b/doc/optimisation.rst @@ -3,7 +3,7 @@ Optimisation Techniques ======================= -The :mod:`bitstring` module aims to be as fast as reasonably possible, and since version 4.1 has used the ``bitarray`` C extension to power its core. +The :mod:`bitstring` module aims to be as fast as reasonably possible, and since version 5.0 has used the ``tibs`` Rust module to power its core. There are however some pointers you should follow to make your code efficient, so if you need things to run faster then this is the section for you. diff --git a/tests/test_bitarray.py b/tests/test_bitarray.py index cffc4426..0954898e 100644 --- a/tests/test_bitarray.py +++ b/tests/test_bitarray.py @@ -6,7 +6,6 @@ import pytest import sys import os -import bitarray import bitstring from bitstring import BitArray, Bits diff --git a/tests/test_bits.py b/tests/test_bits.py index 6a801a0d..04421e67 100644 --- a/tests/test_bits.py +++ b/tests/test_bits.py @@ -2,7 +2,6 @@ import pytest import io import sys -import bitarray import bitstring import array import os @@ -203,15 +202,6 @@ def test_creation_keyword_error(self): with pytest.raises(bitstring.CreationError): Bits(squirrel=5) - def test_creation_from_bitarray_errors(self): - ba = bitarray.bitarray('0101') - with pytest.raises(bitstring.CreationError): - _ = Bits(bitarray=ba, length=5) - with pytest.raises(bitstring.CreationError): - _ = Bits(bitarray=ba, offset=5) - with pytest.raises(bitstring.CreationError): - _ = Bits(ba, length=-1) - def test_creation_from_memoryview(self): x = bytes(bytearray(range(20))) m = memoryview(x[10:15]) From 906411a238ab8350cce2174d585568a1126ef914 Mon Sep 17 00:00:00 2001 From: Scott Griffiths Date: Wed, 11 Mar 2026 21:02:12 +0000 Subject: [PATCH 03/35] Moving the common helpers. --- bitstring/__init__.py | 1 - bitstring/bitarray_.py | 4 +- bitstring/bits.py | 35 +++-- bitstring/bitstore_common_helpers.py | 192 --------------------------- bitstring/bitstore_tibs_helpers.py | 189 ++++++++++++++++++++++++++ bitstring/bitstream.py | 7 +- bitstring/methods.py | 3 +- 7 files changed, 213 insertions(+), 218 deletions(-) delete mode 100644 bitstring/bitstore_common_helpers.py diff --git a/bitstring/__init__.py b/bitstring/__init__.py index 46052b00..894102bf 100644 --- a/bitstring/__init__.py +++ b/bitstring/__init__.py @@ -62,7 +62,6 @@ import sys import bitstring.bitstore_tibs as bitstore import bitstring.bitstore_tibs_helpers as bitstore_helpers -import bitstring.bitstore_common_helpers from .bits import Bits from .bitstring_options import Options diff --git a/bitstring/bitarray_.py b/bitstring/bitarray_.py index 929bcb3f..2ba143e1 100644 --- a/bitstring/bitarray_.py +++ b/bitstring/bitarray_.py @@ -10,7 +10,7 @@ from bitstring.bits import Bits, BitsType, TBits import bitstring.dtypes -common_helpers = bitstring.bitstore_common_helpers +helpers = bitstring.bitstore_helpers MutableBitStore = bitstring.bitstore.MutableBitStore @@ -133,7 +133,7 @@ def __new__(cls: Type[TBits], auto: Optional[Union[BitsType, int]] = None, /, le def fromstring(cls: TBits, s: str, /) -> TBits: """Create a new bitstring from a formatted string.""" x = super().__new__(cls) - b = common_helpers.str_to_bitstore(s) + b = helpers.str_to_bitstore(s) x._bitstore = b._mutable_copy() return x diff --git a/bitstring/bits.py b/bitstring/bits.py index 156c0d84..37b038c2 100644 --- a/bitstring/bits.py +++ b/bitstring/bits.py @@ -20,7 +20,6 @@ ConstBitStore = bitstring.bitstore.ConstBitStore MutableBitStore = bitstring.bitstore.MutableBitStore helpers = bitstring.bitstore_helpers -common_helpers = bitstring.bitstore_common_helpers # Things that can be converted to Bits when a Bits type is needed @@ -496,7 +495,7 @@ def _clear(self) -> None: def _setauto_no_length_or_offset(self, s: BitsType, /) -> None: """Set bitstring from a bitstring, file, bool, array, iterable or string.""" if isinstance(s, str): - self._bitstore = common_helpers.str_to_bitstore(s) + self._bitstore = helpers.str_to_bitstore(s) elif isinstance(s, Bits): self._bitstore = s._bitstore.copy() elif isinstance(s, (bytes, bytearray, memoryview)): @@ -573,31 +572,31 @@ def _setbits(self, bs: BitsType, length: None = None) -> None: self._bitstore = bs._bitstore def _setp3binary(self, f: float) -> None: - self._bitstore = common_helpers.p3binary2bitstore(f) + self._bitstore = helpers.p3binary2bitstore(f) def _setp4binary(self, f: float) -> None: - self._bitstore = common_helpers.p4binary2bitstore(f) + self._bitstore = helpers.p4binary2bitstore(f) def _sete4m3mxfp(self, f: float) -> None: - self._bitstore = common_helpers.e4m3mxfp2bitstore(f) + self._bitstore = helpers.e4m3mxfp2bitstore(f) def _sete5m2mxfp(self, f: float) -> None: - self._bitstore = common_helpers.e5m2mxfp2bitstore(f) + self._bitstore = helpers.e5m2mxfp2bitstore(f) def _sete3m2mxfp(self, f: float) -> None: - self._bitstore = common_helpers.e3m2mxfp2bitstore(f) + self._bitstore = helpers.e3m2mxfp2bitstore(f) def _sete2m3mxfp(self, f: float) -> None: - self._bitstore = common_helpers.e2m3mxfp2bitstore(f) + self._bitstore = helpers.e2m3mxfp2bitstore(f) def _sete2m1mxfp(self, f: float) -> None: - self._bitstore = common_helpers.e2m1mxfp2bitstore(f) + self._bitstore = helpers.e2m1mxfp2bitstore(f) def _sete8m0mxfp(self, f: float) -> None: - self._bitstore = common_helpers.e8m0mxfp2bitstore(f) + self._bitstore = helpers.e8m0mxfp2bitstore(f) def _setmxint(self, f: float) -> None: - self._bitstore = common_helpers.mxint2bitstore(f) + self._bitstore = helpers.mxint2bitstore(f) def _setbytes(self, data: Union[bytearray, bytes, List], length:None = None) -> None: """Set the data from a bytes or bytearray object.""" @@ -788,7 +787,7 @@ def _getbfloatbe(self) -> float: def _setbfloatbe(self, f: Union[float, str], length: Optional[int] = None) -> None: if length is not None and length != 16: raise bitstring.CreationError(f"bfloats must be length 16, received a length of {length} bits.") - self._bitstore = common_helpers.bfloat2bitstore(f, True) + self._bitstore = helpers.bfloat2bitstore(f, True) def _getbfloatle(self) -> float: zero_padded = Bits(16) + self @@ -797,7 +796,7 @@ def _getbfloatle(self) -> float: def _setbfloatle(self, f: Union[float, str], length: Optional[int] = None) -> None: if length is not None and length != 16: raise bitstring.CreationError(f"bfloats must be length 16, received a length of {length} bits.") - self._bitstore = common_helpers.bfloat2bitstore(f, False) + self._bitstore = helpers.bfloat2bitstore(f, False) def _setue(self, i: int) -> None: """Initialise bitstring with unsigned exponential-Golomb code for integer i. @@ -807,7 +806,7 @@ def _setue(self, i: int) -> None: """ if bitstring.options.lsb0: raise bitstring.CreationError("Exp-Golomb codes cannot be used in lsb0 mode.") - self._bitstore = common_helpers.ue2bitstore(i) + self._bitstore = helpers.ue2bitstore(i) def _readue(self, pos: int) -> Tuple[int, int]: """Return interpretation of next bits as unsigned exponential-Golomb code. @@ -864,7 +863,7 @@ def _setse(self, i: int) -> None: """Initialise bitstring with signed exponential-Golomb code for integer i.""" if bitstring.options.lsb0: raise bitstring.CreationError("Exp-Golomb codes cannot be used in lsb0 mode.") - self._bitstore = common_helpers.se2bitstore(i) + self._bitstore = helpers.se2bitstore(i) def _readse(self, pos: int) -> Tuple[int, int]: """Return interpretation of next bits as a signed exponential-Golomb code. @@ -887,7 +886,7 @@ def _setuie(self, i: int) -> None: """ if bitstring.options.lsb0: raise bitstring.CreationError("Exp-Golomb codes cannot be used in lsb0 mode.") - self._bitstore = common_helpers.uie2bitstore(i) + self._bitstore = helpers.uie2bitstore(i) def _readuie(self, pos: int) -> Tuple[int, int]: """Return interpretation of next bits as unsigned interleaved exponential-Golomb code. @@ -914,7 +913,7 @@ def _setsie(self, i: int, ) -> None: """Initialise bitstring with signed interleaved exponential-Golomb code for integer i.""" if bitstring.options.lsb0: raise bitstring.CreationError("Exp-Golomb codes cannot be used in lsb0 mode.") - self._bitstore = common_helpers.sie2bitstore(i) + self._bitstore = helpers.sie2bitstore(i) def _readsie(self, pos: int) -> Tuple[int, int]: """Return interpretation of next bits as a signed interleaved exponential-Golomb code. @@ -1711,7 +1710,7 @@ def copy(self: TBits) -> TBits: def fromstring(cls: TBits, s: str, /) -> TBits: """Create a new bitstring from a formatted string.""" x = super().__new__(cls) - x._bitstore = common_helpers.str_to_bitstore(s) + x._bitstore = helpers.str_to_bitstore(s) return x len = length = property(_getlength, doc="The length of the bitstring in bits. Read only.") diff --git a/bitstring/bitstore_common_helpers.py b/bitstring/bitstore_common_helpers.py deleted file mode 100644 index 5bc409e2..00000000 --- a/bitstring/bitstore_common_helpers.py +++ /dev/null @@ -1,192 +0,0 @@ -from __future__ import annotations - -import struct -import math -from typing import Union, Dict, Callable, Optional -import functools -import bitstring -from bitstring.fp8 import p4binary_fmt, p3binary_fmt -from bitstring.mxfp import (e3m2mxfp_fmt, e2m3mxfp_fmt, e2m1mxfp_fmt, e4m3mxfp_saturate_fmt, - e5m2mxfp_saturate_fmt, e4m3mxfp_overflow_fmt, e5m2mxfp_overflow_fmt) - -helpers = bitstring.bitstore_helpers -ConstBitStore = bitstring.bitstore.ConstBitStore -MutableBitStore = bitstring.bitstore.MutableBitStore - - -CACHE_SIZE = 256 - -@functools.lru_cache(CACHE_SIZE) -def str_to_bitstore(s: str) -> ConstBitStore: - _, tokens = bitstring.utils.tokenparser(s) - constbitstores = [bitstore_from_token(*token) for token in tokens] - return ConstBitStore.join(constbitstores) - - -literal_bit_funcs: Dict[str, Callable[..., ConstBitStore]] = { - '0x': helpers.hex2bitstore, - '0X': helpers.hex2bitstore, - '0b': helpers.bin2bitstore, - '0B': helpers.bin2bitstore, - '0o': helpers.oct2bitstore, - '0O': helpers.oct2bitstore, -} - - -def bitstore_from_token(name: str, token_length: Optional[int], value: Optional[str]) -> ConstBitStore: - if name in literal_bit_funcs: - return literal_bit_funcs[name](value) - try: - d = bitstring.dtypes.Dtype(name, token_length) - except ValueError as e: - raise bitstring.CreationError(f"Can't parse token: {e}") - if value is None and name != 'pad': - raise ValueError(f"Token {name} requires a value.") - bs = d.build(value)._bitstore - if token_length is not None and len(bs) != d.bitlength: - raise bitstring.CreationError(f"Token with length {token_length} packed with value of length {len(bs)} " - f"({name}:{token_length}={value}).") - return bs - - - -def ue2bitstore(i: Union[str, int]) -> ConstBitStore: - i = int(i) - if i < 0: - raise bitstring.CreationError("Cannot use negative initialiser for unsigned exponential-Golomb.") - if i == 0: - return ConstBitStore.from_bin('1') - tmp = i + 1 - leadingzeros = -1 - while tmp > 0: - tmp >>= 1 - leadingzeros += 1 - remainingpart = i + 1 - (1 << leadingzeros) - return ConstBitStore.from_bin('0' * leadingzeros + '1') + helpers.int2bitstore(remainingpart, leadingzeros, False) - - -def se2bitstore(i: Union[str, int]) -> ConstBitStore: - i = int(i) - if i > 0: - u = (i * 2) - 1 - else: - u = -2 * i - return ue2bitstore(u) - - -def uie2bitstore(i: Union[str, int]) -> ConstBitStore: - i = int(i) - if i < 0: - raise bitstring.CreationError("Cannot use negative initialiser for unsigned interleaved exponential-Golomb.") - return ConstBitStore.from_bin('1' if i == 0 else '0' + '0'.join(bin(i + 1)[3:]) + '1') - - -def sie2bitstore(i: Union[str, int]) -> ConstBitStore: - i = int(i) - if i == 0: - return ConstBitStore.from_bin('1') - else: - return uie2bitstore(abs(i)) + (ConstBitStore.from_bin('1') if i < 0 else ConstBitStore.from_bin('0')) - - -def bfloat2bitstore(f: Union[str, float], big_endian: bool) -> ConstBitStore: - f = float(f) - fmt = '>f' if big_endian else ' 0 else float('-inf')) - return ConstBitStore.from_bytes(b[0:2]) if big_endian else ConstBitStore.from_bytes(b[2:4]) - - -def p4binary2bitstore(f: Union[str, float]) -> ConstBitStore: - f = float(f) - u = p4binary_fmt.float_to_int8(f) - return helpers.int2bitstore(u, 8, False) - - -def p3binary2bitstore(f: Union[str, float]) -> ConstBitStore: - f = float(f) - u = p3binary_fmt.float_to_int8(f) - return helpers.int2bitstore(u, 8, False) - - -def e4m3mxfp2bitstore(f: Union[str, float]) -> ConstBitStore: - f = float(f) - if bitstring.options.mxfp_overflow == 'saturate': - u = e4m3mxfp_saturate_fmt.float_to_int(f) - else: - u = e4m3mxfp_overflow_fmt.float_to_int(f) - return helpers.int2bitstore(u, 8, False) - - -def e5m2mxfp2bitstore(f: Union[str, float]) -> ConstBitStore: - f = float(f) - if bitstring.options.mxfp_overflow == 'saturate': - u = e5m2mxfp_saturate_fmt.float_to_int(f) - else: - u = e5m2mxfp_overflow_fmt.float_to_int(f) - return helpers.int2bitstore(u, 8, False) - - -def e3m2mxfp2bitstore(f: Union[str, float]) -> ConstBitStore: - f = float(f) - if math.isnan(f): - raise ValueError("Cannot convert float('nan') to e3m2mxfp format as it has no representation for it.") - u = e3m2mxfp_fmt.float_to_int(f) - return helpers.int2bitstore(u, 6, False) - - -def e2m3mxfp2bitstore(f: Union[str, float]) -> ConstBitStore: - f = float(f) - if math.isnan(f): - raise ValueError("Cannot convert float('nan') to e2m3mxfp format as it has no representation for it.") - u = e2m3mxfp_fmt.float_to_int(f) - return helpers.int2bitstore(u, 6, False) - - -def e2m1mxfp2bitstore(f: Union[str, float]) -> ConstBitStore: - f = float(f) - if math.isnan(f): - raise ValueError("Cannot convert float('nan') to e2m1mxfp format as it has no representation for it.") - u = e2m1mxfp_fmt.float_to_int(f) - return helpers.int2bitstore(u, 4, False) - - -e8m0mxfp_allowed_values = [float(2 ** x) for x in range(-127, 128)] - - -def e8m0mxfp2bitstore(f: Union[str, float]) -> ConstBitStore: - f = float(f) - if math.isnan(f): - return ConstBitStore.from_bin('11111111') - try: - i = e8m0mxfp_allowed_values.index(f) - except ValueError: - raise ValueError(f"{f} is not a valid e8m0mxfp value. It must be exactly 2 ** i, for -127 <= i <= 127 or float('nan') as no rounding will be done.") - return helpers.int2bitstore(i, 8, False) - - -def mxint2bitstore(f: Union[str, float]) -> ConstBitStore: - f = float(f) - if math.isnan(f): - raise ValueError("Cannot convert float('nan') to mxint format as it has no representation for it.") - f *= 2 ** 6 # Remove the implicit scaling factor - if f > 127: # 1 + 63/64 - return ConstBitStore.from_bin('01111111') - if f <= -128: # -2 - return ConstBitStore.from_bin('10000000') - # Want to round to nearest, so move by 0.5 away from zero and round down by converting to int - if f >= 0.0: - f += 0.5 - i = int(f) - # For ties-round-to-even - if f - i == 0.0 and i % 2: - i -= 1 - else: - f -= 0.5 - i = int(f) - if f - i == 0.0 and i % 2: - i += 1 - return helpers.int2bitstore(i, 8, True) \ No newline at end of file diff --git a/bitstring/bitstore_tibs_helpers.py b/bitstring/bitstore_tibs_helpers.py index 3b4f7f54..a6703254 100644 --- a/bitstring/bitstore_tibs_helpers.py +++ b/bitstring/bitstore_tibs_helpers.py @@ -4,6 +4,17 @@ from tibs import Tibs, Mutibs import bitstring + +import struct +import math +from typing import Union, Dict, Callable, Optional +import functools +import bitstring +from bitstring.fp8 import p4binary_fmt, p3binary_fmt +from bitstring.mxfp import (e3m2mxfp_fmt, e2m3mxfp_fmt, e2m1mxfp_fmt, e4m3mxfp_saturate_fmt, + e5m2mxfp_saturate_fmt, e4m3mxfp_overflow_fmt, e5m2mxfp_overflow_fmt) + + MutableBitStore = bitstring.bitstore.MutableBitStore ConstBitStore = bitstring.bitstore.ConstBitStore @@ -76,3 +87,181 @@ def float2bitstore(f: Union[str, float], length: int, big_endian: bool) -> Const if not big_endian: mb.byte_swap() return ConstBitStore.from_tibs(mb.as_tibs()) + + +CACHE_SIZE = 256 + +@functools.lru_cache(CACHE_SIZE) +def str_to_bitstore(s: str) -> ConstBitStore: + _, tokens = bitstring.utils.tokenparser(s) + constbitstores = [bitstore_from_token(*token) for token in tokens] + return ConstBitStore.join(constbitstores) + + +literal_bit_funcs: Dict[str, Callable[..., ConstBitStore]] = { + '0x': hex2bitstore, + '0X': hex2bitstore, + '0b': bin2bitstore, + '0B': bin2bitstore, + '0o': oct2bitstore, + '0O': oct2bitstore, +} + + +def bitstore_from_token(name: str, token_length: Optional[int], value: Optional[str]) -> ConstBitStore: + if name in literal_bit_funcs: + return literal_bit_funcs[name](value) + try: + d = bitstring.dtypes.Dtype(name, token_length) + except ValueError as e: + raise bitstring.CreationError(f"Can't parse token: {e}") + if value is None and name != 'pad': + raise ValueError(f"Token {name} requires a value.") + bs = d.build(value)._bitstore + if token_length is not None and len(bs) != d.bitlength: + raise bitstring.CreationError(f"Token with length {token_length} packed with value of length {len(bs)} " + f"({name}:{token_length}={value}).") + return bs + + + +def ue2bitstore(i: Union[str, int]) -> ConstBitStore: + i = int(i) + if i < 0: + raise bitstring.CreationError("Cannot use negative initialiser for unsigned exponential-Golomb.") + if i == 0: + return ConstBitStore.from_bin('1') + tmp = i + 1 + leadingzeros = -1 + while tmp > 0: + tmp >>= 1 + leadingzeros += 1 + remainingpart = i + 1 - (1 << leadingzeros) + return ConstBitStore.from_bin('0' * leadingzeros + '1') + int2bitstore(remainingpart, leadingzeros, False) + + +def se2bitstore(i: Union[str, int]) -> ConstBitStore: + i = int(i) + if i > 0: + u = (i * 2) - 1 + else: + u = -2 * i + return ue2bitstore(u) + + +def uie2bitstore(i: Union[str, int]) -> ConstBitStore: + i = int(i) + if i < 0: + raise bitstring.CreationError("Cannot use negative initialiser for unsigned interleaved exponential-Golomb.") + return ConstBitStore.from_bin('1' if i == 0 else '0' + '0'.join(bin(i + 1)[3:]) + '1') + + +def sie2bitstore(i: Union[str, int]) -> ConstBitStore: + i = int(i) + if i == 0: + return ConstBitStore.from_bin('1') + else: + return uie2bitstore(abs(i)) + (ConstBitStore.from_bin('1') if i < 0 else ConstBitStore.from_bin('0')) + + +def bfloat2bitstore(f: Union[str, float], big_endian: bool) -> ConstBitStore: + f = float(f) + fmt = '>f' if big_endian else ' 0 else float('-inf')) + return ConstBitStore.from_bytes(b[0:2]) if big_endian else ConstBitStore.from_bytes(b[2:4]) + + +def p4binary2bitstore(f: Union[str, float]) -> ConstBitStore: + f = float(f) + u = p4binary_fmt.float_to_int8(f) + return int2bitstore(u, 8, False) + + +def p3binary2bitstore(f: Union[str, float]) -> ConstBitStore: + f = float(f) + u = p3binary_fmt.float_to_int8(f) + return int2bitstore(u, 8, False) + + +def e4m3mxfp2bitstore(f: Union[str, float]) -> ConstBitStore: + f = float(f) + if bitstring.options.mxfp_overflow == 'saturate': + u = e4m3mxfp_saturate_fmt.float_to_int(f) + else: + u = e4m3mxfp_overflow_fmt.float_to_int(f) + return int2bitstore(u, 8, False) + + +def e5m2mxfp2bitstore(f: Union[str, float]) -> ConstBitStore: + f = float(f) + if bitstring.options.mxfp_overflow == 'saturate': + u = e5m2mxfp_saturate_fmt.float_to_int(f) + else: + u = e5m2mxfp_overflow_fmt.float_to_int(f) + return int2bitstore(u, 8, False) + + +def e3m2mxfp2bitstore(f: Union[str, float]) -> ConstBitStore: + f = float(f) + if math.isnan(f): + raise ValueError("Cannot convert float('nan') to e3m2mxfp format as it has no representation for it.") + u = e3m2mxfp_fmt.float_to_int(f) + return int2bitstore(u, 6, False) + + +def e2m3mxfp2bitstore(f: Union[str, float]) -> ConstBitStore: + f = float(f) + if math.isnan(f): + raise ValueError("Cannot convert float('nan') to e2m3mxfp format as it has no representation for it.") + u = e2m3mxfp_fmt.float_to_int(f) + return int2bitstore(u, 6, False) + + +def e2m1mxfp2bitstore(f: Union[str, float]) -> ConstBitStore: + f = float(f) + if math.isnan(f): + raise ValueError("Cannot convert float('nan') to e2m1mxfp format as it has no representation for it.") + u = e2m1mxfp_fmt.float_to_int(f) + return int2bitstore(u, 4, False) + + +e8m0mxfp_allowed_values = [float(2 ** x) for x in range(-127, 128)] + + +def e8m0mxfp2bitstore(f: Union[str, float]) -> ConstBitStore: + f = float(f) + if math.isnan(f): + return ConstBitStore.from_bin('11111111') + try: + i = e8m0mxfp_allowed_values.index(f) + except ValueError: + raise ValueError(f"{f} is not a valid e8m0mxfp value. It must be exactly 2 ** i, for -127 <= i <= 127 or float('nan') as no rounding will be done.") + return int2bitstore(i, 8, False) + + +def mxint2bitstore(f: Union[str, float]) -> ConstBitStore: + f = float(f) + if math.isnan(f): + raise ValueError("Cannot convert float('nan') to mxint format as it has no representation for it.") + f *= 2 ** 6 # Remove the implicit scaling factor + if f > 127: # 1 + 63/64 + return ConstBitStore.from_bin('01111111') + if f <= -128: # -2 + return ConstBitStore.from_bin('10000000') + # Want to round to nearest, so move by 0.5 away from zero and round down by converting to int + if f >= 0.0: + f += 0.5 + i = int(f) + # For ties-round-to-even + if f - i == 0.0 and i % 2: + i -= 1 + else: + f -= 0.5 + i = int(f) + if f - i == 0.0 and i % 2: + i += 1 + return int2bitstore(i, 8, True) \ No newline at end of file diff --git a/bitstring/bitstream.py b/bitstring/bitstream.py index 0c001583..a3ef50f8 100644 --- a/bitstring/bitstream.py +++ b/bitstring/bitstream.py @@ -7,7 +7,8 @@ import copy import numbers -common_helpers = bitstring.bitstore_common_helpers +helpers = bitstring.bitstore_helpers + TConstBitStream = TypeVar("TConstBitStream", bound='ConstBitStream') @@ -472,7 +473,7 @@ def bytealign(self) -> int: @classmethod def fromstring(cls: TBits, s: str, /) -> TBits: x = super().__new__(cls) - x._bitstore = common_helpers.str_to_bitstore(s) + x._bitstore = helpers.str_to_bitstore(s) x._pos = 0 return x @@ -609,7 +610,7 @@ def __init__(self, auto: Optional[Union[BitsType, int]] = None, /, length: Optio @classmethod def fromstring(cls: TBits, s: str, /) -> TBits: x = super().__new__(cls) - b = common_helpers.str_to_bitstore(s) + b = helpers.str_to_bitstore(s) x._bitstore = b._mutable_copy() x._pos = 0 return x diff --git a/bitstring/methods.py b/bitstring/methods.py index c224dc9a..04f8d368 100644 --- a/bitstring/methods.py +++ b/bitstring/methods.py @@ -8,7 +8,6 @@ MutableBitStore = bitstring.bitstore.MutableBitStore helpers = bitstring.bitstore_helpers -common_helpers = bitstring.bitstore_common_helpers def pack(fmt: Union[str, List[str]], *values, **kwargs) -> BitStream: @@ -78,7 +77,7 @@ def pack(fmt: Union[str, List[str]], *values, **kwargs) -> BitStream: raise CreationError(f"Token with length {length} packed with value of length {len(value)}.") bsl.append(value._bitstore) continue - bsl.append(common_helpers.bitstore_from_token(name, length, value)) + bsl.append(helpers.bitstore_from_token(name, length, value)) except StopIteration: raise CreationError(f"Not enough parameters present to pack according to the " f"format. {len(tokens)} values are needed.") From 732e6eb7ab8ee321dbfd909525f606b9fa28d3d5 Mon Sep 17 00:00:00 2001 From: Scott Griffiths Date: Wed, 11 Mar 2026 21:30:43 +0000 Subject: [PATCH 04/35] Renaming bitstore_tibs_helpers.py to bitstore_helpers.py --- bitstring/__init__.py | 2 +- bitstring/bitarray_.py | 2 +- bitstring/bits.py | 4 +++- bitstring/{bitstore_tibs_helpers.py => bitstore_helpers.py} | 0 bitstring/bitstore_tibs.py | 1 - bitstring/bitstream.py | 3 +-- bitstring/methods.py | 3 +-- 7 files changed, 7 insertions(+), 8 deletions(-) rename bitstring/{bitstore_tibs_helpers.py => bitstore_helpers.py} (100%) diff --git a/bitstring/__init__.py b/bitstring/__init__.py index 894102bf..8fd5b92d 100644 --- a/bitstring/__init__.py +++ b/bitstring/__init__.py @@ -61,7 +61,7 @@ import sys import bitstring.bitstore_tibs as bitstore -import bitstring.bitstore_tibs_helpers as bitstore_helpers +# import bitstring.bitstore_helpers as bitstore_helpers from .bits import Bits from .bitstring_options import Options diff --git a/bitstring/bitarray_.py b/bitstring/bitarray_.py index 2ba143e1..71e984cb 100644 --- a/bitstring/bitarray_.py +++ b/bitstring/bitarray_.py @@ -10,7 +10,7 @@ from bitstring.bits import Bits, BitsType, TBits import bitstring.dtypes -helpers = bitstring.bitstore_helpers +import bitstring.bitstore_helpers as helpers MutableBitStore = bitstring.bitstore.MutableBitStore diff --git a/bitstring/bits.py b/bitstring/bits.py index 37b038c2..466c5a41 100644 --- a/bitstring/bits.py +++ b/bitstring/bits.py @@ -17,9 +17,11 @@ from bitstring.mxfp import e3m2mxfp_fmt, e2m3mxfp_fmt, e2m1mxfp_fmt, e4m3mxfp_saturate_fmt, e5m2mxfp_saturate_fmt from bitstring.bitstring_options import Colour +import bitstring.bitstore_helpers as helpers + ConstBitStore = bitstring.bitstore.ConstBitStore MutableBitStore = bitstring.bitstore.MutableBitStore -helpers = bitstring.bitstore_helpers + # Things that can be converted to Bits when a Bits type is needed diff --git a/bitstring/bitstore_tibs_helpers.py b/bitstring/bitstore_helpers.py similarity index 100% rename from bitstring/bitstore_tibs_helpers.py rename to bitstring/bitstore_helpers.py diff --git a/bitstring/bitstore_tibs.py b/bitstring/bitstore_tibs.py index e6547120..92f508f3 100644 --- a/bitstring/bitstore_tibs.py +++ b/bitstring/bitstore_tibs.py @@ -7,7 +7,6 @@ from bitstring.helpers import offset_slice_indices_lsb0 - class ConstBitStore: """A light wrapper around tibs.Tibs that does the LSB0 stuff""" diff --git a/bitstring/bitstream.py b/bitstring/bitstream.py index a3ef50f8..7ed6ed40 100644 --- a/bitstring/bitstream.py +++ b/bitstring/bitstream.py @@ -6,8 +6,7 @@ from typing import Union, List, Any, Optional, overload, TypeVar, Tuple import copy import numbers - -helpers = bitstring.bitstore_helpers +import bitstring.bitstore_helpers as helpers TConstBitStream = TypeVar("TConstBitStream", bound='ConstBitStream') diff --git a/bitstring/methods.py b/bitstring/methods.py index 04f8d368..c9de3868 100644 --- a/bitstring/methods.py +++ b/bitstring/methods.py @@ -5,10 +5,9 @@ from bitstring.utils import tokenparser from bitstring.exceptions import CreationError from typing import Union, List +import bitstring.bitstore_helpers as helpers MutableBitStore = bitstring.bitstore.MutableBitStore -helpers = bitstring.bitstore_helpers - def pack(fmt: Union[str, List[str]], *values, **kwargs) -> BitStream: """Pack the values according to the format string and return a new BitStream. From 636df3f416d632905b317a7020d991ee6e5cbfaf Mon Sep 17 00:00:00 2001 From: Scott Griffiths Date: Thu, 12 Mar 2026 15:08:22 +0000 Subject: [PATCH 05/35] Renaming _bits to tibs. --- bitstring/bitarray_.py | 6 +- bitstring/bits.py | 4 +- bitstring/bitstore_tibs.py | 225 ++++++++++++++++++------------------- 3 files changed, 114 insertions(+), 121 deletions(-) diff --git a/bitstring/bitarray_.py b/bitstring/bitarray_.py index 71e984cb..e96c8aaa 100644 --- a/bitstring/bitarray_.py +++ b/bitstring/bitarray_.py @@ -279,17 +279,17 @@ def __imul__(self: TBits, n: int) -> TBits: def __ior__(self: TBits, bs: BitsType) -> TBits: bs = self._create_from_bitstype(bs) - self._bitstore |= bs._bitstore + self._bitstore.tibs |= bs._bitstore.tibs return self def __iand__(self: TBits, bs: BitsType) -> TBits: bs = self._create_from_bitstype(bs) - self._bitstore &= bs._bitstore + self._bitstore.tibs &= bs._bitstore.tibs return self def __ixor__(self: TBits, bs: BitsType) -> TBits: bs = self._create_from_bitstype(bs) - self._bitstore ^= bs._bitstore + self._bitstore.tibs ^= bs._bitstore.tibs return self def _replace(self, old: Bits, new: Bits, start: int, end: int, count: int, bytealigned: Optional[bool]) -> int: diff --git a/bitstring/bits.py b/bitstring/bits.py index 466c5a41..2e411d52 100644 --- a/bitstring/bits.py +++ b/bitstring/bits.py @@ -378,7 +378,7 @@ def __mul__(self: TBits, n: int, /) -> TBits: def _imul(self: TBits, n: int, /) -> TBits: """Concatenate n copies of self in place. Return self.""" - self._bitstore.__imul__(n) + self._bitstore.tibs *= n return self def __rmul__(self: TBits, n: int, /) -> TBits: @@ -1026,7 +1026,7 @@ def _readtoken(self, name: str, pos: int, length: Optional[int]) -> Tuple[Union[ def _addright(self, bs: Bits, /) -> None: """Add a bitstring to the RHS of the current bitstring.""" - self._bitstore += bs._bitstore + self._bitstore.tibs += bs._bitstore.tibs def _addleft(self, bs: Bits, /) -> None: """Prepend a bitstring to the current bitstring.""" diff --git a/bitstring/bitstore_tibs.py b/bitstring/bitstore_tibs.py index 92f508f3..11905fed 100644 --- a/bitstring/bitstore_tibs.py +++ b/bitstring/bitstore_tibs.py @@ -10,76 +10,76 @@ class ConstBitStore: """A light wrapper around tibs.Tibs that does the LSB0 stuff""" - __slots__ = ('_bits',) + __slots__ = ('tibs',) def __init__(self, initializer: Union[Tibs, None] = None) -> None: if initializer is not None: - self._bits = initializer + self.tibs = initializer else: - self._bits = Tibs() + self.tibs = Tibs() @classmethod def join(cls, bitstores: Iterable[ConstBitStore], /) -> ConstBitStore: x = super().__new__(cls) - x._bits = Tibs.from_joined(b._bits for b in bitstores) + x.tibs = Tibs.from_joined(b.tibs for b in bitstores) return x @classmethod def from_zeros(cls, i: int): x = super().__new__(cls) - x._bits = Tibs.from_zeros(i) + x.tibs = Tibs.from_zeros(i) return x @classmethod def from_tibs(cls, tb: Tibs): x = super().__new__(cls) - x._bits = tb + x.tibs = tb return x @classmethod def from_bytes(cls, b: Union[bytes, bytearray, memoryview], /) -> ConstBitStore: x = super().__new__(cls) - x._bits = Tibs.from_bytes(b) + x.tibs = Tibs.from_bytes(b) return x @classmethod def frombuffer(cls, buffer, /, length: Optional[int] = None) -> ConstBitStore: x = super().__new__(cls) # TODO: tibs needs a Tibs.from_buffer method. - x._bits = Tibs.from_bytes(bytes(buffer)) + x.tibs = Tibs.from_bytes(bytes(buffer)) if length is not None: if length < 0: raise CreationError("Can't create bitstring with a negative length.") - if length > len(x._bits): + if length > len(x.tibs): raise CreationError( - f"Can't create bitstring with a length of {length} from {len(x._bits)} bits of data.") + f"Can't create bitstring with a length of {length} from {len(x.tibs)} bits of data.") return x.getslice(0, length) if length is not None else x @classmethod def from_bin(cls, s: str) -> ConstBitStore: x = super().__new__(cls) - x._bits = Tibs.from_bin(s) + x.tibs = Tibs.from_bin(s) return x def set(self, value, pos) -> None: - self._bits.set(value, pos) + self.tibs.set(value, pos) def to_bytes(self, pad_at_end: bool = True) -> bytes: - excess_bits = len(self._bits) % 8 + excess_bits = len(self.tibs) % 8 if excess_bits != 0: # Pad with zeros to make full bytes if pad_at_end: - padded_bits = self._bits.to_mutibs().extend(Mutibs.from_zeros(8 - excess_bits)) + padded_bits = self.tibs.to_mutibs().extend(Mutibs.from_zeros(8 - excess_bits)) else: - padded_bits = self._bits.to_mutibs().extend_left(Mutibs.from_zeros(8 - excess_bits)) + padded_bits = self.tibs.to_mutibs().extend_left(Mutibs.from_zeros(8 - excess_bits)) return padded_bits.to_bytes() - return self._bits.to_bytes() + return self.tibs.to_bytes() def to_u(self) -> int: if len(self) > 128: return int.from_bytes(self.to_bytes(pad_at_end=False), byteorder="big", signed=False) try: - return self._bits.to_u() + return self.tibs.to_u() except OverflowError as e: raise ValueError(e) @@ -87,58 +87,58 @@ def to_i(self) -> int: if len(self) > 128: return int.from_bytes(self.to_bytes(pad_at_end=False), byteorder="big", signed=True) try: - return self._bits.to_i() + return self.tibs.to_i() except OverflowError as e: raise ValueError(e) def to_hex(self) -> str: - return self._bits.to_hex() + return self.tibs.to_hex() def to_bin(self) -> str: - return self._bits.to_bin() + return self.tibs.to_bin() def to_oct(self) -> str: - return self._bits.to_oct() + return self.tibs.to_oct() def __add__(self, other: ConstBitStore, /) -> ConstBitStore: - newbits = self._bits + other._bits + newbits = self.tibs + other.tibs return ConstBitStore.from_tibs(newbits) def __eq__(self, other: Any, /) -> bool: - return self._bits == other._bits + return self.tibs == other.tibs def __and__(self, other: ConstBitStore, /) -> ConstBitStore: - return ConstBitStore.from_tibs(self._bits & other._bits) + return ConstBitStore.from_tibs(self.tibs & other.tibs) def __or__(self, other: ConstBitStore, /) -> ConstBitStore: - return ConstBitStore.from_tibs(self._bits | other._bits) + return ConstBitStore.from_tibs(self.tibs | other.tibs) def __xor__(self, other: ConstBitStore, /) -> ConstBitStore: - return ConstBitStore.from_tibs(self._bits ^ other._bits) + return ConstBitStore.from_tibs(self.tibs ^ other.tibs) def __invert__(self) -> ConstBitStore: - return ConstBitStore.from_tibs(~self._bits) + return ConstBitStore.from_tibs(~self.tibs) def find(self, bs: ConstBitStore, start: int, end: int, bytealigned: bool = False) -> int | None: assert start >= 0 - return self._bits.find(bs._bits, start, end, byte_aligned=bytealigned) + return self.tibs.find(bs.tibs, start, end, byte_aligned=bytealigned) def rfind(self, bs: ConstBitStore, start: int, end: int, bytealigned: bool = False) -> int | None: assert start >= 0 - return self._bits.rfind(bs._bits, start, end, byte_aligned=bytealigned) + return self.tibs.rfind(bs.tibs, start, end, byte_aligned=bytealigned) def findall_msb0(self, bs: ConstBitStore, start: int, end: int, bytealigned: bool = False) -> Iterator[int]: - x = self._bits - for p in x.find_all(bs._bits, start=start, end=end, byte_aligned=bytealigned): + x = self.tibs + for p in x.find_all(bs.tibs, start=start, end=end, byte_aligned=bytealigned): yield p def rfindall_msb0(self, bs: ConstBitStore, start: int, end: int, bytealigned: bool = False) -> Iterator[int]: - x = self._bits - for p in x.rfind_all(bs._bits, start=start, end=end, byte_aligned=bytealigned): + x = self.tibs + for p in x.rfind_all(bs.tibs, start=start, end=end, byte_aligned=bytealigned): yield p def count(self, value, /) -> int: - return self._bits.count(value) + return self.tibs.count(value) def __iter__(self) -> Iterable[bool]: length = len(self) @@ -147,113 +147,113 @@ def __iter__(self) -> Iterable[bool]: def _mutable_copy(self) -> MutableBitStore: """Always creates a copy, even if instance is immutable.""" - return MutableBitStore.from_mutibs(self._bits.to_mutibs()) + return MutableBitStore.from_mutibs(self.tibs.to_mutibs()) def copy(self) -> ConstBitStore: - return self if isinstance(self._bits, Tibs) else self._mutable_copy() + return self if isinstance(self.tibs, Tibs) else self._mutable_copy() def __getitem__(self, item: Union[int, slice], /) -> Union[int, ConstBitStore]: # Use getindex or getslice instead raise NotImplementedError def getindex_msb0(self, index: int, /) -> bool: - return self._bits.__getitem__(index) + return self.tibs.__getitem__(index) def getslice_withstep_msb0(self, key: slice, /) -> ConstBitStore: - return ConstBitStore(self._bits.__getitem__(key)) + return ConstBitStore(self.tibs.__getitem__(key)) def getslice_withstep_lsb0(self, key: slice, /) -> ConstBitStore: key = offset_slice_indices_lsb0(key, len(self)) - return ConstBitStore(self._bits.__getitem__(key)) + return ConstBitStore(self.tibs.__getitem__(key)) def getslice_msb0(self, start: Optional[int], stop: Optional[int], /) -> ConstBitStore: - return ConstBitStore(self._bits[start:stop]) + return ConstBitStore(self.tibs[start:stop]) def getslice_lsb0(self, start: Optional[int], stop: Optional[int], /) -> ConstBitStore: s = offset_slice_indices_lsb0(slice(start, stop, None), len(self)) - return ConstBitStore(self._bits[s.start:s.stop]) + return ConstBitStore(self.tibs[s.start:s.stop]) def getindex_lsb0(self, index: int, /) -> bool: - return self._bits.__getitem__(-index - 1) + return self.tibs.__getitem__(-index - 1) def any(self) -> bool: - return self._bits.any() + return self.tibs.any() def all(self) -> bool: - return self._bits.all() + return self.tibs.all() def __len__(self) -> int: - return len(self._bits) + return len(self.tibs) class MutableBitStore: """A light wrapper around tibs.Mutibs that does the LSB0 stuff""" - __slots__ = ('_bits',) + __slots__ = ('tibs',) - def __init__(self, initializer: Union[Mutibs, Tibs, None] = None) -> None: + def __init__(self, initializer: Union[Mutibs, None] = None) -> None: if initializer is not None: - self._bits = initializer + self.tibs = initializer else: - self._bits = Mutibs() + self.tibs = Mutibs() @classmethod def from_zeros(cls, i: int): x = super().__new__(cls) - x._bits = Mutibs.from_zeros(i) + x.tibs = Mutibs.from_zeros(i) return x @classmethod def from_mutibs(cls, mb: Mutibs): assert isinstance(mb, Mutibs) x = super().__new__(cls) - x._bits = mb + x.tibs = mb return x @classmethod def from_bytes(cls, b: Union[bytes, bytearray, memoryview], /) -> MutableBitStore: x = super().__new__(cls) - x._bits = Mutibs.from_bytes(b) + x.tibs = Mutibs.from_bytes(b) return x @classmethod def frombuffer(cls, buffer, /, length: Optional[int] = None) -> MutableBitStore: x = super().__new__(cls) - # TODO: tibs needs a Bits.from_buffer method. - x._bits = Mutibs.from_bytes(bytes(buffer)) + # TODO: tibs needs a Mutits.from_buffer method. + x.tibs = Mutibs.from_bytes(bytes(buffer)) if length is not None: if length < 0: raise CreationError("Can't create bitstring with a negative length.") - if length > len(x._bits): + if length > len(x.tibs): raise CreationError( - f"Can't create bitstring with a length of {length} from {len(x._bits)} bits of data.") + f"Can't create bitstring with a length of {length} from {len(x.tibs)} bits of data.") return x.getslice(0, length) if length is not None else x @classmethod def from_bin(cls, s: str) -> MutableBitStore: x = super().__new__(cls) - x._bits = Mutibs.from_bin(s) + x.tibs = Mutibs.from_bin(s) return x def set(self, value, pos) -> None: - self._bits.set(value, pos) + self.tibs.set(value, pos) def to_bytes(self, pad_at_end: bool = True) -> bytes: - excess_bits = len(self._bits) % 8 + excess_bits = len(self.tibs) % 8 if excess_bits != 0: # Pad with zeros to make full bytes if pad_at_end: - padded_bits = self._bits + Mutibs.from_zeros(8 - excess_bits) + padded_bits = self.tibs + Mutibs.from_zeros(8 - excess_bits) else: - padded_bits = Mutibs.from_zeros(8 - excess_bits) + self._bits + padded_bits = Mutibs.from_zeros(8 - excess_bits) + self.tibs return padded_bits.to_bytes() - return self._bits.to_bytes() + return self.tibs.to_bytes() def to_u(self) -> int: if len(self) > 128: return int.from_bytes(self.to_bytes(pad_at_end=False), byteorder="big", signed=False) try: - return self._bits.to_u() + return self.tibs.to_u() except OverflowError as e: raise ValueError(e) @@ -261,101 +261,94 @@ def to_i(self) -> int: if len(self) > 128: return int.from_bytes(self.to_bytes(pad_at_end=False), byteorder="big", signed=True) try: - return self._bits.to_i() + return self.tibs.to_i() except OverflowError as e: raise ValueError(e) def to_hex(self) -> str: - return self._bits.to_hex() + return self.tibs.to_hex() def to_bin(self) -> str: - return self._bits.to_bin() + return self.tibs.to_bin() def to_oct(self) -> str: - return self._bits.to_oct() - - def __imul__(self, n: int, /) -> None: - self._bits *= n + return self.tibs.to_oct() def __ilshift__(self, n: int, /) -> None: - self._bits <<= n + self.tibs <<= n def __irshift__(self, n: int, /) -> None: - self._bits >>= n - - def __iadd__(self, other: MutableBitStore, /) -> MutableBitStore: - self._bits += other._bits - return self + self.tibs >>= n def __add__(self, other: MutableBitStore, /) -> MutableBitStore: bs = self._mutable_copy() - bs += other + bs.tibs += other.tibs return bs def __eq__(self, other: Any, /) -> bool: - return self._bits == other._bits + return self.tibs == other.tibs def __and__(self, other: MutableBitStore, /) -> MutableBitStore: - return MutableBitStore.from_mutibs(self._bits & other._bits) + return MutableBitStore.from_mutibs(self.tibs & other.tibs) def __or__(self, other: MutableBitStore, /) -> MutableBitStore: - return MutableBitStore.from_mutibs(self._bits | other._bits) + return MutableBitStore.from_mutibs(self.tibs | other.tibs) def __xor__(self, other: MutableBitStore, /) -> MutableBitStore: - return MutableBitStore.from_mutibs(self._bits ^ other._bits) + return MutableBitStore.from_mutibs(self.tibs ^ other.tibs) def __iand__(self, other: MutableBitStore, /) -> MutableBitStore: - self._bits &= other._bits + self.tibs &= other.tibs return self def __ior__(self, other: MutableBitStore, /) -> MutableBitStore: - self._bits |= other._bits + self.tibs |= other.tibs return self def __ixor__(self, other: MutableBitStore, /) -> MutableBitStore: - self._bits ^= other._bits + self.tibs ^= other.tibs return self def __invert__(self) -> MutableBitStore: - return MutableBitStore.from_mutibs(~self._bits) + return MutableBitStore.from_mutibs(~self.tibs) def find(self, bs: MutableBitStore, start: int, end: int, bytealigned: bool = False) -> int: assert start >= 0 - return self._bits.find(bs._bits, start, end, byte_aligned=bytealigned) + return self.tibs.find(bs.tibs, start, end, byte_aligned=bytealigned) def rfind(self, bs: MutableBitStore, start: int, end: int, bytealigned: bool = False): assert start >= 0 - return self._bits.rfind(bs._bits, start, end, byte_aligned=bytealigned) + return self.tibs.rfind(bs.tibs, start, end, byte_aligned=bytealigned) def findall_msb0(self, bs: MutableBitStore, start: int, end: int, bytealigned: bool = False) -> Iterator[int]: - x = self._bits.to_tibs() - for p in x.find_all(bs._bits, start=start, end=end, byte_aligned=bytealigned): + x = self.tibs.to_tibs() + for p in x.find_all(bs.tibs, start=start, end=end, byte_aligned=bytealigned): yield p def rfindall_msb0(self, bs: MutableBitStore, start: int, end: int, bytealigned: bool = False) -> Iterator[int]: - x = self._bits.to_tibs() - for p in x.rfind_all(bs._bits, start=start, end=end, byte_aligned=bytealigned): + x = self.tibs.to_tibs() + for p in x.rfind_all(bs.tibs, start=start, end=end, byte_aligned=bytealigned): yield p def count(self, value, /) -> int: - return self._bits.count(value) + return self.tibs.count(value) def clear(self) -> None: - self._bits.clear() + self.tibs.clear() def reverse(self) -> None: - self._bits.reverse() + self.tibs.reverse() def __iter__(self) -> Iterable[bool]: for i in range(len(self)): yield self.getindex(i) def extend_left(self, other: MutableBitStore, /) -> None: - self._bits.extend_left(other._bits) + self.tibs.extend_left(other.tibs) def _mutable_copy(self) -> MutableBitStore: """Always creates a copy, even if instance is immutable.""" - return MutableBitStore.from_mutibs(self._bits.__copy__()) + return MutableBitStore.from_mutibs(self.tibs.__copy__()) def copy(self) -> MutableBitStore: return self._mutable_copy() @@ -365,24 +358,24 @@ def __getitem__(self, item: Union[int, slice], /) -> Union[int, MutableBitStore] raise NotImplementedError def getindex_msb0(self, index: int, /) -> bool: - return self._bits.__getitem__(index) + return self.tibs.__getitem__(index) def getslice_withstep_msb0(self, key: slice, /) -> MutableBitStore: - return MutableBitStore(self._bits.__getitem__(key)) + return MutableBitStore(self.tibs.__getitem__(key)) def getslice_withstep_lsb0(self, key: slice, /) -> MutableBitStore: key = offset_slice_indices_lsb0(key, len(self)) - return MutableBitStore(self._bits.__getitem__(key)) + return MutableBitStore(self.tibs.__getitem__(key)) def getslice_msb0(self, start: Optional[int], stop: Optional[int], /) -> MutableBitStore: - return MutableBitStore(self._bits[start:stop]) + return MutableBitStore(self.tibs[start:stop]) def getslice_lsb0(self, start: Optional[int], stop: Optional[int], /) -> MutableBitStore: s = offset_slice_indices_lsb0(slice(start, stop, None), len(self)) - return MutableBitStore(self._bits[s.start:s.stop]) + return MutableBitStore(self.tibs[s.start:s.stop]) def getindex_lsb0(self, index: int, /) -> bool: - return self._bits.__getitem__(-index - 1) + return self.tibs.__getitem__(-index - 1) @overload def setitem_lsb0(self, key: int, value: int, /) -> None: @@ -395,45 +388,45 @@ def setitem_lsb0(self, key: slice, value: MutableBitStore, /) -> None: def setitem_lsb0(self, key: Union[int, slice], value: Union[int, MutableBitStore], /) -> None: if isinstance(key, slice): new_slice = offset_slice_indices_lsb0(key, len(self)) - self._bits.__setitem__(new_slice, value._bits) + self.tibs.__setitem__(new_slice, value.tibs) else: - self._bits.__setitem__(-key - 1, bool(value)) + self.tibs.__setitem__(-key - 1, bool(value)) def delitem_lsb0(self, key: Union[int, slice], /) -> None: if isinstance(key, slice): new_slice = offset_slice_indices_lsb0(key, len(self)) - self._bits.__delitem__(new_slice) + self.tibs.__delitem__(new_slice) else: - self._bits.__delitem__(-key - 1) + self.tibs.__delitem__(-key - 1) def invert_msb0(self, index: Optional[int] = None, /) -> None: if index is not None: - self._bits.invert(index) + self.tibs.invert(index) else: - self._bits.invert() + self.tibs.invert() def invert_lsb0(self, index: Optional[int] = None, /) -> None: if index is not None: - self._bits.invert(-index - 1) + self.tibs.invert(-index - 1) else: - self._bits.invert() + self.tibs.invert() def any(self) -> bool: - return self._bits.any() + return self.tibs.any() def all(self) -> bool: - return self._bits.all() + return self.tibs.all() def __len__(self) -> int: - return len(self._bits) + return len(self.tibs) def setitem_msb0(self, key, value, /): if isinstance(value, (MutableBitStore, ConstBitStore)): - self._bits.__setitem__(key, value._bits) + self.tibs.__setitem__(key, value.tibs) else: if isinstance(key, slice): key = range(*key.indices(len(self))) - self._bits.set(value, key) + self.tibs.set(value, key) def delitem_msb0(self, key, /): - self._bits.__delitem__(key) + self.tibs.__delitem__(key) From ad7f087f8cfe2ea2a19a0e4d7e77531e29f6532e Mon Sep 17 00:00:00 2001 From: Scott Griffiths Date: Thu, 12 Mar 2026 18:07:41 +0000 Subject: [PATCH 06/35] Fix for signed ints > 128 bits not being interpreted correctly. Removing some unneeded methods. --- bitstring/bits.py | 4 +- bitstring/bitstore_tibs.py | 67 ++--- tests/test_bits.py | 539 ++++++++++++++++++++----------------- 3 files changed, 309 insertions(+), 301 deletions(-) diff --git a/bitstring/bits.py b/bitstring/bits.py index 2e411d52..54b10a6f 100644 --- a/bitstring/bits.py +++ b/bitstring/bits.py @@ -1512,9 +1512,7 @@ def count(self, value: Any) -> int: 7 """ - # count the number of 1s (from which it's easy to work out the 0s). - count = self._bitstore.count(1) - return count if value else len(self) - count + return self._bitstore.tibs.count(bool(value)) @staticmethod def _format_bits(bits: Bits, bits_per_group: int, sep: str, dtype: Dtype, diff --git a/bitstring/bitstore_tibs.py b/bitstring/bitstore_tibs.py index 11905fed..87443418 100644 --- a/bitstring/bitstore_tibs.py +++ b/bitstring/bitstore_tibs.py @@ -61,23 +61,20 @@ def from_bin(cls, s: str) -> ConstBitStore: x.tibs = Tibs.from_bin(s) return x - def set(self, value, pos) -> None: - self.tibs.set(value, pos) - - def to_bytes(self, pad_at_end: bool = True) -> bytes: - excess_bits = len(self.tibs) % 8 - if excess_bits != 0: - # Pad with zeros to make full bytes - if pad_at_end: - padded_bits = self.tibs.to_mutibs().extend(Mutibs.from_zeros(8 - excess_bits)) - else: - padded_bits = self.tibs.to_mutibs().extend_left(Mutibs.from_zeros(8 - excess_bits)) - return padded_bits.to_bytes() - return self.tibs.to_bytes() + def to_bytes(self) -> bytes: + padding = 8 - len(self.tibs) % 8 + if padding == 8: + return self.tibs.to_bytes() + return (self.tibs + [0] * padding).to_bytes() def to_u(self) -> int: if len(self) > 128: - return int.from_bytes(self.to_bytes(pad_at_end=False), byteorder="big", signed=False) + padding = 8 - len(self.tibs) % 8 + if padding == 8: + b = self.tibs.to_bytes() + else: + b = ([0] * padding + self.tibs).to_bytes() + return int.from_bytes(b, byteorder="big", signed=False) try: return self.tibs.to_u() except OverflowError as e: @@ -85,7 +82,13 @@ def to_u(self) -> int: def to_i(self) -> int: if len(self) > 128: - return int.from_bytes(self.to_bytes(pad_at_end=False), byteorder="big", signed=True) + padding = 8 - len(self.tibs) % 8 + if padding == 8: + b = self.tibs.to_bytes() + else: + pad_bit = self.tibs[0] # Keep sign when padding + b = ([pad_bit] * padding + self.tibs).to_bytes() + return int.from_bytes(b, byteorder="big", signed=True) try: return self.tibs.to_i() except OverflowError as e: @@ -137,9 +140,6 @@ def rfindall_msb0(self, bs: ConstBitStore, start: int, end: int, bytealigned: bo for p in x.rfind_all(bs.tibs, start=start, end=end, byte_aligned=bytealigned): yield p - def count(self, value, /) -> int: - return self.tibs.count(value) - def __iter__(self) -> Iterable[bool]: length = len(self) for i in range(length): @@ -216,28 +216,12 @@ def from_bytes(cls, b: Union[bytes, bytearray, memoryview], /) -> MutableBitStor x.tibs = Mutibs.from_bytes(b) return x - @classmethod - def frombuffer(cls, buffer, /, length: Optional[int] = None) -> MutableBitStore: - x = super().__new__(cls) - # TODO: tibs needs a Mutits.from_buffer method. - x.tibs = Mutibs.from_bytes(bytes(buffer)) - if length is not None: - if length < 0: - raise CreationError("Can't create bitstring with a negative length.") - if length > len(x.tibs): - raise CreationError( - f"Can't create bitstring with a length of {length} from {len(x.tibs)} bits of data.") - return x.getslice(0, length) if length is not None else x - @classmethod def from_bin(cls, s: str) -> MutableBitStore: x = super().__new__(cls) x.tibs = Mutibs.from_bin(s) return x - def set(self, value, pos) -> None: - self.tibs.set(value, pos) - def to_bytes(self, pad_at_end: bool = True) -> bytes: excess_bits = len(self.tibs) % 8 if excess_bits != 0: @@ -297,18 +281,6 @@ def __or__(self, other: MutableBitStore, /) -> MutableBitStore: def __xor__(self, other: MutableBitStore, /) -> MutableBitStore: return MutableBitStore.from_mutibs(self.tibs ^ other.tibs) - def __iand__(self, other: MutableBitStore, /) -> MutableBitStore: - self.tibs &= other.tibs - return self - - def __ior__(self, other: MutableBitStore, /) -> MutableBitStore: - self.tibs |= other.tibs - return self - - def __ixor__(self, other: MutableBitStore, /) -> MutableBitStore: - self.tibs ^= other.tibs - return self - def __invert__(self) -> MutableBitStore: return MutableBitStore.from_mutibs(~self.tibs) @@ -330,9 +302,6 @@ def rfindall_msb0(self, bs: MutableBitStore, start: int, end: int, bytealigned: for p in x.rfind_all(bs.tibs, start=start, end=end, byte_aligned=bytealigned): yield p - def count(self, value, /) -> int: - return self.tibs.count(value) - def clear(self) -> None: self.tibs.clear() diff --git a/tests/test_bits.py b/tests/test_bits.py index 04421e67..b030acd9 100644 --- a/tests/test_bits.py +++ b/tests/test_bits.py @@ -7,24 +7,27 @@ import os import re from bitstring import InterpretError, Bits, BitArray -from hypothesis import given, assume, reproduce_failure, settings +from hypothesis import given, settings import hypothesis.strategies as st from bitstring.helpers import offset_slice_indices_lsb0 -sys.path.insert(0, '..') +sys.path.insert(0, "..") THIS_DIR = os.path.dirname(os.path.abspath(__file__)) + def remove_unprintable(s: str) -> str: - colour_escape = re.compile(r'(?:\x1B[@-_])[0-?]*[ -/]*[@-~]') - return colour_escape.sub('', s) + colour_escape = re.compile(r"(?:\x1B[@-_])[0-?]*[ -/]*[@-~]") + return colour_escape.sub("", s) @settings(max_examples=500) -@given(length=st.integers(0, 9), - start=st.integers(-20, 20), - stop=st.integers(-20, 20), - step=st.integers(0, 7)) +@given( + length=st.integers(0, 9), + start=st.integers(-20, 20), + stop=st.integers(-20, 20), + step=st.integers(0, 7), +) def test_lsb0_slicing(length, start, stop, step): if start == -20: start = None @@ -41,17 +44,17 @@ def test_lsb0_slicing(length, start, stop, step): values1 = values_fwd[start1:stop1:step1] lsb0key = offset_slice_indices_lsb0(slice(start, stop, step), length) - values2 = values_bwd[lsb0key.start:lsb0key.stop:lsb0key.step] + values2 = values_bwd[lsb0key.start : lsb0key.stop : lsb0key.step] values2.reverse() assert values1 == values2 class TestCreation: def test_creation_from_bytes(self): - s = Bits(bytes=b'\xa0\xff') - assert (s.len, s.hex) == (16, 'a0ff') - s = Bits(bytes=b'abc', length=0) - assert s == '' + s = Bits(bytes=b"\xa0\xff") + assert (s.len, s.hex) == (16, "a0ff") + s = Bits(bytes=b"abc", length=0) + assert s == "" @given(st.binary()) def test_creation_from_bytes_roundtrip(self, data): @@ -60,52 +63,52 @@ def test_creation_from_bytes_roundtrip(self, data): def test_creation_from_bytes_errors(self): with pytest.raises(bitstring.CreationError): - Bits(bytes=b'abc', length=25) + Bits(bytes=b"abc", length=25) def test_creation_from_data_with_offset(self): - s1 = Bits(bytes=b'\x0b\x1c\x2f', offset=0, length=20) - s2 = Bits(bytes=b'\xa0\xb1\xC2', offset=4) - assert (s2.len, s2.hex) == (20, '0b1c2') - assert (s1.len, s1.hex) == (20, '0b1c2') + s1 = Bits(bytes=b"\x0b\x1c\x2f", offset=0, length=20) + s2 = Bits(bytes=b"\xa0\xb1\xc2", offset=4) + assert (s2.len, s2.hex) == (20, "0b1c2") + assert (s1.len, s1.hex) == (20, "0b1c2") assert s1 == s2 def test_creation_from_hex(self): - s = Bits(hex='0xA0ff') - assert (s.len, s.hex) == (16, 'a0ff') - s = Bits(hex='0x0x0X') - assert (s.length, s.hex) == (0, '') + s = Bits(hex="0xA0ff") + assert (s.len, s.hex) == (16, "a0ff") + s = Bits(hex="0x0x0X") + assert (s.length, s.hex) == (0, "") def test_creation_from_hex_with_whitespace(self): - s = Bits(hex=' \n0 X a 4e \r3 \n') - assert s.hex == 'a4e3' + s = Bits(hex=" \n0 X a 4e \r3 \n") + assert s.hex == "a4e3" - @pytest.mark.parametrize("bad_val", ['0xx0', '0xX0', '0Xx0', '-2e']) + @pytest.mark.parametrize("bad_val", ["0xx0", "0xX0", "0Xx0", "-2e"]) def test_creation_from_hex_errors(self, bad_val: str): with pytest.raises(bitstring.CreationError): Bits(hex=bad_val) with pytest.raises(bitstring.CreationError): - Bits('0x2', length=2) + Bits("0x2", length=2) with pytest.raises(bitstring.CreationError): - Bits('0x3', offset=1) + Bits("0x3", offset=1) def test_creation_from_bin(self): - s = Bits(bin='1010000011111111') - assert (s.length, s.hex) == (16, 'a0ff') - s = Bits(bin='00')[:1] - assert s.bin == '0' - s = Bits(bin=' 0000 \n 0001\r ') - assert s.bin == '00000001' + s = Bits(bin="1010000011111111") + assert (s.length, s.hex) == (16, "a0ff") + s = Bits(bin="00")[:1] + assert s.bin == "0" + s = Bits(bin=" 0000 \n 0001\r ") + assert s.bin == "00000001" def test_creation_from_bin_with_whitespace(self): - s = Bits(bin=' \r\r\n0 B 00 1 1 \t0 ') - assert s.bin == '00110' + s = Bits(bin=" \r\r\n0 B 00 1 1 \t0 ") + assert s.bin == "00110" def test_creation_from_oct_errors(self): - s = Bits('0b00011') + s = Bits("0b00011") with pytest.raises(bitstring.InterpretError): _ = s.oct with pytest.raises(bitstring.CreationError): - _ = Bits('oct=8') + _ = Bits("oct=8") def test_creation_from_uint_with_offset(self): with pytest.raises(bitstring.CreationError): @@ -125,11 +128,11 @@ def test_creation_from_uint_errors(self): def test_creation_from_int(self): s = Bits(int=0, length=4) - assert s.bin == '0000' + assert s.bin == "0000" s = Bits(int=1, length=2) - assert s.bin == '01' + assert s.bin == "01" s = Bits(int=-1, length=11) - assert s.bin == '11111111111' + assert s.bin == "11111111111" s = Bits(int=12, length=7) assert s.int == 12 s = Bits(int=-243, length=108) @@ -160,8 +163,8 @@ def test_creation_from_se_errors(self): with pytest.raises(bitstring.CreationError): Bits(se=-5, length=33) with pytest.raises(bitstring.CreationError): - Bits('se2=0') - s = Bits(bin='001000') + Bits("se2=0") + s = Bits(bin="001000") with pytest.raises(bitstring.InterpretError): _ = s.se @@ -178,23 +181,23 @@ def test_creation_from_ue_errors(self): Bits(ue=-1) with pytest.raises(bitstring.CreationError): Bits(ue=1, length=12) - s = Bits(bin='10') + s = Bits(bin="10") with pytest.raises(bitstring.InterpretError): _ = s.ue def test_creation_from_bool(self): - a = Bits('bool=1') - assert a == 'bool=1' - b = Bits('bool:1=0') + a = Bits("bool=1") + assert a == "bool=1" + b = Bits("bool:1=0") assert b == [0] - c = bitstring.pack('bool=1, 2*bool', 0, 1) - assert c == '0b101' - d = bitstring.pack('bool:1=1, 2*bool1', 1, 0) - assert d == '0b110' + c = bitstring.pack("bool=1, 2*bool", 0, 1) + assert c == "0b101" + d = bitstring.pack("bool:1=1, 2*bool1", 1, 0) + assert d == "0b110" def test_creation_from_bool_errors(self): with pytest.raises(ValueError): - _ = Bits('bool=3') + _ = Bits("bool=3") with pytest.raises(bitstring.CreationError): _ = Bits(bool=0, length=2) @@ -206,45 +209,45 @@ def test_creation_from_memoryview(self): x = bytes(bytearray(range(20))) m = memoryview(x[10:15]) b = Bits(m) - assert b.unpack('5*u8') == [10, 11, 12, 13, 14] + assert b.unpack("5*u8") == [10, 11, 12, 13, 14] class TestInitialisation: def test_empty_init(self): a = Bits() - assert a == '' + assert a == "" def test_no_pos(self): - a = Bits('0xabcdef') + a = Bits("0xabcdef") with pytest.raises(AttributeError): _ = a.pos def test_find(self): - a = Bits('0xabcd') - r = a.find('0xbc') + a = Bits("0xabcd") + r = a.find("0xbc") assert r[0] == 4 - r = a.find('0x23462346246', bytealigned=True) + r = a.find("0x23462346246", bytealigned=True) assert not r def test_rfind(self): - a = Bits('0b11101010010010') - b = a.rfind('0b010') + a = Bits("0b11101010010010") + b = a.rfind("0b010") assert b[0] == 11 def test_find_all(self): - a = Bits('0b0010011') + a = Bits("0b0010011") b = list(a.findall([1])) assert b == [2, 5, 6] - t = BitArray('0b10') - tp = list(t.findall('0b1')) + t = BitArray("0b10") + tp = list(t.findall("0b1")) assert tp == [0] class TestCut: def test_cut(self): - s = Bits('0b000111'*10) + s = Bits("0b000111" * 10) for t in s.cut(6): - assert t.bin == '000111' + assert t.bin == "000111" class TestInterleavedExpGolomb: @@ -274,7 +277,7 @@ def test_interpretation(self): assert Bits(sie=x).sie == x def test_errors(self): - for f in ['sie=100, 0b1001', '0b00', 'uie=100, 0b1001']: + for f in ["sie=100, 0b1001", "0b00", "uie=100, 0b1001"]: s = Bits.fromstring(f) with pytest.raises(bitstring.InterpretError): _ = s.sie @@ -286,20 +289,20 @@ def test_errors(self): class TestFileBased: def setup_method(self): - filename = os.path.join(THIS_DIR, 'smalltestfile') + filename = os.path.join(THIS_DIR, "smalltestfile") self.a = Bits(filename=filename) self.b = Bits(filename=filename, offset=16) self.c = Bits(filename=filename, offset=20, length=16) self.d = Bits(filename=filename, offset=20, length=4) def test_creation_with_offset(self): - assert str(self.a) == '0x0123456789abcdef' - assert str(self.b) == '0x456789abcdef' - assert str(self.c) == '0x5678' + assert str(self.a) == "0x0123456789abcdef" + assert str(self.b) == "0x456789abcdef" + assert str(self.c) == "0x5678" def test_bit_operators(self): x = self.b[4:20] - assert x == '0x5678' + assert x == "0x5678" assert (x & self.c).hex == self.c.hex assert self.c ^ self.b[4:20] == Bits(16) assert self.a[23:36] | self.c[3:] == self.c[3:] @@ -308,14 +311,14 @@ def test_bit_operators(self): assert repr(y) == repr(self.c) def test_addition(self): - _ = self.d + '0x1' + _ = self.d + "0x1" x = self.a[20:24] + self.c[-4:] + self.c[8:12] - assert x == '0x587' + assert x == "0x587" x = self.b + x - assert x.h == '456789abcdef587' + assert x.h == "456789abcdef587" x = BitArray(x) del x[12:24] - assert x == '0x456abcdef587' + assert x == "0x456abcdef587" class TestComparisons: @@ -333,21 +336,21 @@ def test_unorderable(self): class TestSubclassing: - def test_is_instance(self): class SubBits(bitstring.Bits): pass + a = SubBits() assert isinstance(a, SubBits) def test_class_type(self): class SubBits(bitstring.Bits): pass + assert SubBits().__class__ == SubBits class TestLongBoolConversion: - def test_long_bool(self): a = Bits(1000) b = bool(a) @@ -355,47 +358,45 @@ def test_long_bool(self): class TestPadToken: - def test_creation(self): - a = Bits.fromstring('pad:10') + a = Bits.fromstring("pad:10") assert a == Bits(10) - b = Bits('pad:0') + b = Bits("pad:0") assert b == Bits() - c = Bits('0b11, pad:1, 0b111') - assert c == Bits('0b110111') + c = Bits("0b11, pad:1, 0b111") + assert c == Bits("0b110111") def test_pack(self): - s = bitstring.pack('0b11, pad:3, 0b1') - assert s.bin == '110001' - d = bitstring.pack('pad:c', c=12) + s = bitstring.pack("0b11, pad:3, 0b1") + assert s.bin == "110001" + d = bitstring.pack("pad:c", c=12) assert d == Bits(12) - e = bitstring.pack('0xf, uint12, pad:1, bin, pad4, 0b10', 0, '111') - assert e.bin == '11110000000000000111000010' + e = bitstring.pack("0xf, uint12, pad:1, bin, pad4, 0b10", 0, "111") + assert e.bin == "11110000000000000111000010" def test_unpack(self): - s = Bits('0b111000111') - x, y = s.unpack('3, pad:3, 3') - assert (x, y.u) == ('0b111', 7) - x, y = s.unpack('2, pad2, bin') - assert (x.u2, y) == (3, '00111') - x = s.unpack('pad:1, pad:2, pad:3') + s = Bits("0b111000111") + x, y = s.unpack("3, pad:3, 3") + assert (x, y.u) == ("0b111", 7) + x, y = s.unpack("2, pad2, bin") + assert (x.u2, y) == (3, "00111") + x = s.unpack("pad:1, pad:2, pad:3") assert x == [] def test_unpack_bug(self): - t = Bits('0o755, ue=12, int3=-1') - a, b = t.unpack('pad:9, ue, int3') + t = Bits("0o755, ue=12, int3=-1") + a, b = t.unpack("pad:9, ue, int3") assert (a, b) == (12, -1) class TestModifiedByAddingBug: - def test_adding(self): - a = Bits('0b0') - b = Bits('0b11') + a = Bits("0b0") + b = Bits("0b11") c = a + b - assert c == '0b011' - assert a == '0b0' - assert b == '0b11' + assert c == "0b011" + assert a == "0b0" + assert b == "0b11" def test_adding2(self): a = Bits(100) @@ -407,32 +408,30 @@ def test_adding2(self): class TestWrongTypeBug: - def test_append_to_bits(self): a = Bits(BitArray()) with pytest.raises(AttributeError): - a.append('0b1') + a.append("0b1") assert type(a) == Bits b = bitstring.ConstBitStream(bitstring.BitStream()) assert type(b) == bitstring.ConstBitStream class TestInitFromArray: - - @given(st.sampled_from(['B', 'H', 'I', 'L', 'Q', 'f', 'd'])) + @given(st.sampled_from(["B", "H", "I", "L", "Q", "f", "d"])) def test_empty_array(self, t): a = array.array(t) b = Bits(a) assert b.length == 0 def test_single_byte(self): - a = array.array('B', b'\xff') + a = array.array("B", b"\xff") b = Bits(a) assert b.length == 8 - assert b.hex == 'ff' + assert b.hex == "ff" def test_signed_short(self): - a = array.array('h') + a = array.array("h") a.append(10) a.append(-1) b = Bits(a) @@ -440,15 +439,14 @@ def test_signed_short(self): assert b.bytes == a.tobytes() def test_double(self): - a = array.array('d', [0.0, 1.0, 2.5]) + a = array.array("d", [0.0, 1.0, 2.5]) b = Bits(a) assert b.length == 192 - c, d, e = b.unpack('3*floatne:64') + c, d, e = b.unpack("3*floatne:64") assert (c, d, e) == (0.0, 1.0, 2.5) class TestIteration: - def test_iterate_empty_bits(self): assert list(Bits([])) == [] assert list(Bits([1, 0])[1:1]) == [] @@ -458,43 +456,39 @@ def test_iterate_non_empty_bits(self): assert list(Bits([1, 0, 0, 1])[1:3]) == [False, False] def test_iterate_long_bits(self): - assert list(Bits([1, 0]) * 1024) == \ - [True, False] * 1024 + assert list(Bits([1, 0]) * 1024) == [True, False] * 1024 - -class TestContainsBug: +class TestContainsBug: def test_contains(self): - a = Bits('0b1, 0x0001dead0001') - assert '0xdead' in a - assert not '0xfeed' in a + a = Bits("0b1, 0x0001dead0001") + assert "0xdead" in a + assert "0xfeed" not in a - assert '0b1' in Bits('0xf') - assert not '0b0' in Bits('0xf') + assert "0b1" in Bits("0xf") + assert "0b0" not in Bits("0xf") class TestByteStoreImmutablity: - def test_immutability_bug_append(self): - a = Bits('0b111') - b = a + '0b000' + a = Bits("0b111") + b = a + "0b000" c = BitArray(b) c[1] = 0 - assert c.bin == '101000' - assert a.b3 == '111' - assert b.bin == '111000' + assert c.bin == "101000" + assert a.b3 == "111" + assert b.bin == "111000" def test_immutability_bug_prepend(self): - a = Bits('0b111') - b = '0b000' + a + a = Bits("0b111") + b = "0b000" + a c = BitArray(b) c[1] = 1 - assert b.bin == '000111' - assert c.bin == '010111' + assert b.bin == "000111" + assert c.bin == "010111" class TestLsb0Indexing: - @classmethod def setup_class(cls): bitstring.lsb0 = True @@ -504,7 +498,7 @@ def teardown_class(cls): bitstring.lsb0 = False def test_get_single_bit(self): - a = Bits('0b000001111') + a = Bits("0b000001111") assert a[0] is True assert a[3] is True assert a[4] is False @@ -519,54 +513,53 @@ def test_get_single_bit(self): _ = a[-10] def test_simple_slicing(self): - a = Bits('0xabcdef') - assert a[0:4] == '0xf' - assert a[4:8] == '0xe' - assert a[:] == '0xabcdef' - assert a[4:] == '0xabcde' - assert a[-4:] == '0xa' - assert a[-8:-4] == '0xb' - assert a[:-8] == '0xcdef' + a = Bits("0xabcdef") + assert a[0:4] == "0xf" + assert a[4:8] == "0xe" + assert a[:] == "0xabcdef" + assert a[4:] == "0xabcde" + assert a[-4:] == "0xa" + assert a[-8:-4] == "0xb" + assert a[:-8] == "0xcdef" def test_extended_slicing(self): - a = Bits('0b0100000100100100') - assert a[2::3] == '0b10111' + a = Bits("0b0100000100100100") + assert a[2::3] == "0b10111" def test_all(self): - a = Bits('0b000111') + a = Bits("0b000111") assert a.all(1, [0, 1, 2]) assert a.all(0, [3, 4, 5]) def test_any(self): - a = Bits('0b00000110') + a = Bits("0b00000110") assert a.any(1, [0, 1]) assert a.any(0, [5, 6]) def test_startswith(self): - a = Bits('0b0000000111') - assert a.startswith('0b111') - assert not a.startswith('0b0') - assert a.startswith('0b011', start=1) - assert not a.startswith('0b0111', end=3) - assert a.startswith('0b0111', end=4) + a = Bits("0b0000000111") + assert a.startswith("0b111") + assert not a.startswith("0b0") + assert a.startswith("0b011", start=1) + assert not a.startswith("0b0111", end=3) + assert a.startswith("0b0111", end=4) def test_ends_with(self): - a = Bits('0x1234abcd') - assert a.endswith('0x123') - assert not a.endswith('0xabcd') + a = Bits("0x1234abcd") + assert a.endswith("0x123") + assert not a.endswith("0xabcd") def test_lsb0_slicing_error(self): - a = Bits('0b01') + a = Bits("0b01") b = a[::-1] - assert b == '0b10' - t = Bits('0xf0a')[::-1] - assert t == '0x50f' - s = Bits('0xf0a')[::-1][::-1] - assert s == '0xf0a' + assert b == "0b10" + t = Bits("0xf0a")[::-1] + assert t == "0x50f" + s = Bits("0xf0a")[::-1][::-1] + assert s == "0xf0a" class TestLsb0Interpretations: - @classmethod def setup_class(cls): bitstring.lsb0 = True @@ -576,8 +569,8 @@ def teardown_class(cls): bitstring.lsb0 = False def test_uint(self): - a = Bits('0x01') - assert a == '0b00000001' + a = Bits("0x01") + assert a == "0b00000001" assert a.uint == 1 assert a[0] is True @@ -603,112 +596,136 @@ def test_golomb(self): _ = Bits(sie=2) def test_bytes(self): - a = Bits.fromstring('0xabcdef') + a = Bits.fromstring("0xabcdef") b = a.bytes - assert b == b'\xab\xcd\xef' + assert b == b"\xab\xcd\xef" b = a.bytes3 - assert b == b'\xab\xcd\xef' + assert b == b"\xab\xcd\xef" class TestUnderscoresInLiterals: - def test_hex_creation(self): - a = Bits(hex='ab_cd__ef') - assert a.hex == 'abcdef' - b = Bits('0x0102_0304') + a = Bits(hex="ab_cd__ef") + assert a.hex == "abcdef" + b = Bits("0x0102_0304") assert b.uint == 0x0102_0304 def test_binary_creation(self): - a = Bits(bin='0000_0001_0010') - assert a.bin == '000000010010' - b = Bits.fromstring('0b0011_1100_1111_0000') - assert b.bin == '0011110011110000' + a = Bits(bin="0000_0001_0010") + assert a.bin == "000000010010" + b = Bits.fromstring("0b0011_1100_1111_0000") + assert b.bin == "0011110011110000" v = 0b1010_0000 c = Bits(uint=0b1010_0000, length=8) assert c.uint == v def test_octal_creation(self): - a = Bits(oct='0011_2233_4455_6677') + a = Bits(oct="0011_2233_4455_6677") assert a.uint == 0o001122334455_6677 - b = Bits('0o123_321_123_321') + b = Bits("0o123_321_123_321") assert b.uint == 0o123_321_123321 class TestPrettyPrinting: - def test_simplest_cases(self): - a = Bits('0b101011110000') + a = Bits("0b101011110000") s = io.StringIO() a.pp(stream=s) - assert remove_unprintable(s.getvalue()) == """ [ + assert ( + remove_unprintable(s.getvalue()) + == """ [ 0: 10101111 0000 ] """ + ) s = io.StringIO() - a.pp('hex', stream=s) - assert remove_unprintable(s.getvalue()) == """ [ + a.pp("hex", stream=s) + assert ( + remove_unprintable(s.getvalue()) + == """ [ 0: af 0 ] """ + ) s = io.StringIO() - a.pp('oct', stream=s) - assert remove_unprintable(s.getvalue()) == """ [ + a.pp("oct", stream=s) + assert ( + remove_unprintable(s.getvalue()) + == """ [ 0: 5360 ] """ + ) def test_small_width(self): a = Bits(20) s = io.StringIO() - a.pp(fmt='b', stream=s, width=5) - assert remove_unprintable(s.getvalue()) == """ [ + a.pp(fmt="b", stream=s, width=5) + assert ( + remove_unprintable(s.getvalue()) + == """ [ 0: 00000000 8: 00000000 16: 0000 ] """ + ) def test_separator(self): - a = Bits('0x0f0f')*9 + a = Bits("0x0f0f") * 9 s = io.StringIO() - a.pp('hex:32', sep='!-!', stream=s) - assert remove_unprintable(s.getvalue()) == """ [ + a.pp("hex:32", sep="!-!", stream=s) + assert ( + remove_unprintable(s.getvalue()) + == """ [ 0: 0f0f0f0f!-!0f0f0f0f!-!0f0f0f0f!-!0f0f0f0f ] + trailing_bits = 0x0f0f """ + ) def test_multi_line(self): a = Bits(100) s = io.StringIO() - a.pp('bin', sep='', stream=s, width=80) - assert remove_unprintable(s.getvalue()) == """ [ + a.pp("bin", sep="", stream=s, width=80) + assert ( + remove_unprintable(s.getvalue()) + == """ [ 0: 000000000000000000000000000000000000000000000000000000000000000000000000 72: 0000000000000000000000000000 ] """ + ) def test_multiformat(self): - a = Bits('0b1111000011110000') + a = Bits("0b1111000011110000") s = io.StringIO() - a.pp(stream=s, fmt='bin, hex') - assert remove_unprintable(s.getvalue()) == """ [ + a.pp(stream=s, fmt="bin, hex") + assert ( + remove_unprintable(s.getvalue()) + == """ [ 0: 11110000 11110000 : f0 f0 ] """ + ) s = io.StringIO() - a.pp(stream=s, fmt='hex, bin:12') - assert remove_unprintable(s.getvalue()) == """ [ + a.pp(stream=s, fmt="hex, bin:12") + assert ( + remove_unprintable(s.getvalue()) + == """ [ 0: f0f : 111100001111 ] + trailing_bits = 0x0 """ + ) def test_multi_line_multi_format(self): a = Bits(int=-1, length=112) s = io.StringIO() - a.pp(stream=s, fmt='bin:8, hex:8', width=42) - assert remove_unprintable(s.getvalue()) == """ [ + a.pp(stream=s, fmt="bin:8, hex:8", width=42) + assert ( + remove_unprintable(s.getvalue()) + == """ [ 0: 11111111 11111111 11111111 : ff ff ff 24: 11111111 11111111 11111111 : ff ff ff 48: 11111111 11111111 11111111 : ff ff ff @@ -716,9 +733,12 @@ def test_multi_line_multi_format(self): 96: 11111111 11111111 : ff ff ] """ + ) s = io.StringIO() - a.pp(stream=s, fmt='bin, hex', width=41) - assert remove_unprintable(s.getvalue()) == """ [ + a.pp(stream=s, fmt="bin, hex", width=41) + assert ( + remove_unprintable(s.getvalue()) + == """ [ 0: 11111111 11111111 : ff ff 16: 11111111 11111111 : ff ff 32: 11111111 11111111 : ff ff @@ -728,29 +748,33 @@ def test_multi_line_multi_format(self): 96: 11111111 11111111 : ff ff ] """ + ) a = bytearray(range(0, 256)) b = Bits(bytes=a) s = io.StringIO() - b.pp(stream=s, fmt='bytes') - assert remove_unprintable(s.getvalue()) == r""" [ + b.pp(stream=s, fmt="bytes") + assert ( + remove_unprintable(s.getvalue()) + == r""" [ 0: ĀāĂă ĄąĆć ĈĉĊċ ČčĎď ĐđĒē ĔĕĖė ĘęĚě ĜĝĞğ !"# $%&' ()*+ ,-./ 0123 4567 89:; <=>? @ABC DEFG HIJK LMNO PQRS TUVW XYZ[ 736: \]^_ `abc defg hijk lmno pqrs tuvw xyz{ |}~ſ ƀƁƂƃ ƄƅƆƇ ƈƉƊƋ ƌƍƎƏ ƐƑƒƓ ƔƕƖƗ Ƙƙƚƛ ƜƝƞƟ ƠơƢƣ ƤƥƦƧ ƨƩƪƫ ƬƭƮƯ ưƱƲƳ ƴƵƶƷ 1472: Ƹƹƺƻ Ƽƽƾƿ ǀǁǂǃ DŽDždžLJ LjljNJNj njǍǎǏ ǐǑǒǓ ǔǕǖǗ ǘǙǚǛ ǜǝǞǟ ǠǡǢǣ ǤǥǦǧ ǨǩǪǫ ǬǭǮǯ ǰDZDzdz ǴǵǶǷ ǸǹǺǻ ǼǽǾÿ ] """ + ) def test_group_size_errors(self): a = Bits(120) with pytest.raises(ValueError): - a.pp('hex:3') + a.pp("hex:3") with pytest.raises(ValueError): - a.pp('hex:4, oct') + a.pp("hex:4, oct") def test_zero_group_size(self): a = Bits(600) s = io.StringIO() - a.pp('b0', stream=s, show_offset=False) + a.pp("b0", stream=s, show_offset=False) expected_output = """ [ 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 @@ -763,7 +787,7 @@ def test_zero_group_size(self): a = Bits(400) s = io.StringIO() - a.pp(stream=s, fmt='hex:0', show_offset=False, width=80) + a.pp(stream=s, fmt="hex:0", show_offset=False, width=80) expected_output = """ [ 00000000000000000000000000000000000000000000000000000000000000000000000000000000 00000000000000000000 @@ -773,7 +797,7 @@ def test_zero_group_size(self): s = io.StringIO() a = Bits(uint=10, length=48) - a.pp(stream=s, width=20, fmt='hex:0, oct:0', show_offset=False) + a.pp(stream=s, width=20, fmt="hex:0, oct:0", show_offset=False) expected_output = """ [ 000000 : 00000000 00000a : 00000012 @@ -782,9 +806,9 @@ def test_zero_group_size(self): assert remove_unprintable(s.getvalue()) == expected_output def test_oct(self): - a = Bits('0o01234567'*20) + a = Bits("0o01234567" * 20) s = io.StringIO() - a.pp(stream=s, fmt='o', show_offset=False, width=20) + a.pp(stream=s, fmt="o", show_offset=False, width=20) expected_output = """ [ 0123 4567 0123 4567 0123 4567 0123 4567 @@ -801,7 +825,7 @@ def test_oct(self): assert remove_unprintable(s.getvalue()) == expected_output t = io.StringIO() - a.pp('h, oct:0', width=1, show_offset=False, stream=t) + a.pp("h, oct:0", width=1, show_offset=False, stream=t) expected_output = """ [ 053977 : 01234567 053977 : 01234567 @@ -828,31 +852,28 @@ def test_oct(self): assert remove_unprintable(t.getvalue()) == expected_output def test_bytes(self): - a = Bits(bytes=b'helloworld!!'*5) + a = Bits(bytes=b"helloworld!!" * 5) s = io.StringIO() - a.pp(stream=s, fmt='bytes', show_offset=False, width=48) - expected_output = ( -""" [ + a.pp(stream=s, fmt="bytes", show_offset=False, width=48) + expected_output = """ [ hell owor ld!! hell owor ld!! hell owor ld!! hell owor ld!! hell owor ld!! ] -""") +""" assert remove_unprintable(s.getvalue()) == expected_output s = io.StringIO() - a.pp(stream=s, fmt='bytes0', show_offset=False, width=40) - expected_output = ( -""" [ + a.pp(stream=s, fmt="bytes0", show_offset=False, width=40) + expected_output = """ [ helloworld!!helloworld!!helloworld!!hell oworld!!helloworld!! ] """ - ) assert remove_unprintable(s.getvalue()) == expected_output def test_bool(self): - a = Bits('0b1100') + a = Bits("0b1100") s = io.StringIO() - a.pp(stream=s, fmt='bool', show_offset=False, width=20) + a.pp(stream=s, fmt="bool", show_offset=False, width=20) expected_output = """ [ 1 1 0 0 ] @@ -861,26 +882,24 @@ def test_bool(self): class TestPrettyPrintingErrors: - def test_wrong_formats(self): - a = Bits('0x12341234') + a = Bits("0x12341234") with pytest.raises(ValueError): - a.pp('binary') + a.pp("binary") with pytest.raises(ValueError): - a.pp('bin, bin, bin') + a.pp("bin, bin, bin") def test_interpret_problems(self): a = Bits(7) with pytest.raises(InterpretError): - a.pp('oct') + a.pp("oct") with pytest.raises(InterpretError): - a.pp('hex') + a.pp("hex") with pytest.raises(InterpretError): - a.pp('bin, bytes') + a.pp("bin, bytes") class TestPrettyPrinting_LSB0: - def setup_method(self) -> None: bitstring.lsb0 = True @@ -888,38 +907,49 @@ def teardown_method(self) -> None: bitstring.lsb0 = False def test_bin(self): - a = Bits(bin='1111 0000 0000 1111 1010') + a = Bits(bin="1111 0000 0000 1111 1010") s = io.StringIO() - a.pp('bin', stream=s, width=5) - assert remove_unprintable(s.getvalue()) == """ [ + a.pp("bin", stream=s, width=5) + assert ( + remove_unprintable(s.getvalue()) + == """ [ 11111010 :0 00000000 :8 1111 :16 ] """ + ) -class TestPrettyPrinting_NewFormats: +class TestPrettyPrinting_NewFormats: def test_float(self): - a = Bits('float32=10.5') + a = Bits("float32=10.5") s = io.StringIO() - a.pp('float32', stream=s) - assert remove_unprintable(s.getvalue()) == """ [ + a.pp("float32", stream=s) + assert ( + remove_unprintable(s.getvalue()) + == """ [ 0: 10.5 ] """ + ) s = io.StringIO() - a.pp('float16', stream=s) - assert remove_unprintable(s.getvalue()) == """ [ + a.pp("float16", stream=s) + assert ( + remove_unprintable(s.getvalue()) + == """ [ 0: 2.578125 0.0 ] """ + ) def test_uint(self): a = Bits().join([Bits(uint=x, length=12) for x in range(40, 105)]) s = io.StringIO() - a.pp('uint, h12', stream=s) - assert remove_unprintable(s.getvalue()) == """ [ + a.pp("uint, h12", stream=s) + assert ( + remove_unprintable(s.getvalue()) + == """ [ 0: 40 41 42 43 44 45 46 47 48 49 50 51 : 028 029 02a 02b 02c 02d 02e 02f 030 031 032 033 144: 52 53 54 55 56 57 58 59 60 61 62 63 : 034 035 036 037 038 039 03a 03b 03c 03d 03e 03f 288: 64 65 66 67 68 69 70 71 72 73 74 75 : 040 041 042 043 044 045 046 047 048 049 04a 04b @@ -928,51 +958,62 @@ def test_uint(self): 720: 100 101 102 103 104 : 064 065 066 067 068 ] """ + ) def test_float(self): - a = BitArray(float=76.25, length=64) + '0b11111' + a = BitArray(float=76.25, length=64) + "0b11111" s = io.StringIO() - a.pp('i64, float', stream=s) - assert remove_unprintable(s.getvalue()) == """ [ + a.pp("i64, float", stream=s) + assert ( + remove_unprintable(s.getvalue()) + == """ [ 0: 4635066033680416768 : 76.25 ] + trailing_bits = 0b11111 """ + ) -class TestCopy: +class TestCopy: def test_copy_method(self): - s = Bits('0xc00dee') + s = Bits("0xc00dee") t = s.copy() assert s == t class TestNativeEndianIntegers: - def test_uintne(self): s = Bits(uintne=454, length=160) - t = Bits('uintne160=454') + t = Bits("uintne160=454") assert s == t def test_intne(self): s = Bits(intne=-1000, length=64) - t = Bits('intne:64=-1000') + t = Bits("intne:64=-1000") assert s == t class TestNonNativeEndianIntegers: - def setup_method(self) -> None: - bitstring.byteorder = 'little' if bitstring.byteorder == 'big' else 'little' + bitstring.byteorder = "little" if bitstring.byteorder == "big" else "little" def teardown_method(self) -> None: self.setup_method() def test_uintne(self): s = Bits(uintne=454, length=160) - t = Bits('uintne160=454') + t = Bits("uintne160=454") assert s == t def test_intne(self): s = Bits(intne=-1000, length=64) - t = Bits('intne:64=-1000') + t = Bits("intne:64=-1000") assert s == t + + +def test_large_ints(): + s = Bits(int=-1, length=123) + assert s.int == -1 + s = Bits(int=-1, length=201) + assert s.int == -1 + s = Bits(uint=12, length=201) + assert s.uint == 12 From cbd1f8f2e8bef06bcf6889d61bc371b2dbaf1674 Mon Sep 17 00:00:00 2001 From: Scott Griffiths Date: Fri, 13 Mar 2026 21:49:59 +0000 Subject: [PATCH 07/35] Using the constructors instead of from_tibs and from_mutibs methods. --- bitstring/bitarray_.py | 5 +--- bitstring/bits.py | 7 ++--- bitstring/bitstore_helpers.py | 12 ++++----- bitstring/bitstore_tibs.py | 49 +++++++++++------------------------ 4 files changed, 24 insertions(+), 49 deletions(-) diff --git a/bitstring/bitarray_.py b/bitstring/bitarray_.py index e96c8aaa..ff6d6a18 100644 --- a/bitstring/bitarray_.py +++ b/bitstring/bitarray_.py @@ -121,10 +121,7 @@ def __new__(cls: Type[TBits], auto: Optional[Union[BitsType, int]] = None, /, le x = super(Bits, cls).__new__(cls) if auto is None and not kwargs: # No initialiser so fill with zero bits up to length - if length is not None: - x._bitstore = MutableBitStore.from_zeros(length) - else: - x._bitstore = MutableBitStore() + x._bitstore = MutableBitStore.from_zeros(length if length is not None else 0) return x x._initialise(auto, length, offset, immutable=False, **kwargs) return x diff --git a/bitstring/bits.py b/bitstring/bits.py index 54b10a6f..f0faa4a6 100644 --- a/bitstring/bits.py +++ b/bitstring/bits.py @@ -118,10 +118,7 @@ def __new__(cls: Type[TBits], auto: Optional[Union[BitsType, int]] = None, /, le x = super().__new__(cls) if auto is None and not kwargs: # No initialiser so fill with zero bits up to length - if length is not None: - x._bitstore = ConstBitStore.from_zeros(length) - else: - x._bitstore = ConstBitStore() + x._bitstore = ConstBitStore.from_zeros(length if length is not None else 0) return x x._initialise(auto, length, offset, immutable=True, **kwargs) return x @@ -1406,7 +1403,7 @@ def join(self: TBits, sequence: Iterable[Any]) -> TBits: sequence -- A sequence of bitstrings. """ - bs = MutableBitStore() + bs = MutableBitStore.from_zeros(0) if len(self) == 0: # Optimised version that doesn't need to add self between every item for item in sequence: diff --git a/bitstring/bitstore_helpers.py b/bitstring/bitstore_helpers.py index a6703254..4a4eb4b9 100644 --- a/bitstring/bitstore_helpers.py +++ b/bitstring/bitstore_helpers.py @@ -25,21 +25,21 @@ def bin2bitstore(binstring: str) -> ConstBitStore: binstring = tidy_input_string(binstring) binstring = binstring.replace('0b', '') mb = Tibs.from_bin(binstring) - return ConstBitStore.from_tibs(mb) + return ConstBitStore(mb) def hex2bitstore(hexstring: str) -> ConstBitStore: hexstring = tidy_input_string(hexstring) hexstring = hexstring.replace('0x', '') mb = Tibs.from_hex(hexstring) - return ConstBitStore.from_tibs(mb) + return ConstBitStore(mb) def oct2bitstore(octstring: str) -> ConstBitStore: octstring = tidy_input_string(octstring) octstring = octstring.replace('0o', '') mb = Tibs.from_oct(octstring) - return ConstBitStore.from_tibs(mb) + return ConstBitStore(mb) def int2bitstore(i: int, length: int, signed: bool) -> ConstBitStore: @@ -58,7 +58,7 @@ def int2bitstore(i: int, length: int, signed: bool) -> ConstBitStore: mb = Tibs.from_bytes(b) if offset != 8: mb = mb[offset:] - return ConstBitStore.from_tibs(mb) + return ConstBitStore(mb) def intle2bitstore(i: int, length: int, signed: bool) -> ConstBitStore: @@ -78,7 +78,7 @@ def intle2bitstore(i: int, length: int, signed: bool) -> ConstBitStore: mb = Mutibs.from_bytes(b) if offset != 8: mb = mb[offset:] - return ConstBitStore.from_tibs(mb.as_tibs()) + return ConstBitStore(mb.as_tibs()) def float2bitstore(f: Union[str, float], length: int, big_endian: bool) -> ConstBitStore: @@ -86,7 +86,7 @@ def float2bitstore(f: Union[str, float], length: int, big_endian: bool) -> Const mb = Mutibs.from_f(f, length) if not big_endian: mb.byte_swap() - return ConstBitStore.from_tibs(mb.as_tibs()) + return ConstBitStore(mb.as_tibs()) CACHE_SIZE = 256 diff --git a/bitstring/bitstore_tibs.py b/bitstring/bitstore_tibs.py index 87443418..5e465bf1 100644 --- a/bitstring/bitstore_tibs.py +++ b/bitstring/bitstore_tibs.py @@ -12,11 +12,8 @@ class ConstBitStore: __slots__ = ('tibs',) - def __init__(self, initializer: Union[Tibs, None] = None) -> None: - if initializer is not None: - self.tibs = initializer - else: - self.tibs = Tibs() + def __init__(self, initializer: Tibs) -> None: + self.tibs = initializer @classmethod def join(cls, bitstores: Iterable[ConstBitStore], /) -> ConstBitStore: @@ -30,12 +27,6 @@ def from_zeros(cls, i: int): x.tibs = Tibs.from_zeros(i) return x - @classmethod - def from_tibs(cls, tb: Tibs): - x = super().__new__(cls) - x.tibs = tb - return x - @classmethod def from_bytes(cls, b: Union[bytes, bytearray, memoryview], /) -> ConstBitStore: x = super().__new__(cls) @@ -105,22 +96,22 @@ def to_oct(self) -> str: def __add__(self, other: ConstBitStore, /) -> ConstBitStore: newbits = self.tibs + other.tibs - return ConstBitStore.from_tibs(newbits) + return ConstBitStore(newbits) def __eq__(self, other: Any, /) -> bool: return self.tibs == other.tibs def __and__(self, other: ConstBitStore, /) -> ConstBitStore: - return ConstBitStore.from_tibs(self.tibs & other.tibs) + return ConstBitStore(self.tibs & other.tibs) def __or__(self, other: ConstBitStore, /) -> ConstBitStore: - return ConstBitStore.from_tibs(self.tibs | other.tibs) + return ConstBitStore(self.tibs | other.tibs) def __xor__(self, other: ConstBitStore, /) -> ConstBitStore: - return ConstBitStore.from_tibs(self.tibs ^ other.tibs) + return ConstBitStore(self.tibs ^ other.tibs) def __invert__(self) -> ConstBitStore: - return ConstBitStore.from_tibs(~self.tibs) + return ConstBitStore(~self.tibs) def find(self, bs: ConstBitStore, start: int, end: int, bytealigned: bool = False) -> int | None: assert start >= 0 @@ -147,7 +138,7 @@ def __iter__(self) -> Iterable[bool]: def _mutable_copy(self) -> MutableBitStore: """Always creates a copy, even if instance is immutable.""" - return MutableBitStore.from_mutibs(self.tibs.to_mutibs()) + return MutableBitStore(self.tibs.to_mutibs()) def copy(self) -> ConstBitStore: return self if isinstance(self.tibs, Tibs) else self._mutable_copy() @@ -191,11 +182,8 @@ class MutableBitStore: __slots__ = ('tibs',) - def __init__(self, initializer: Union[Mutibs, None] = None) -> None: - if initializer is not None: - self.tibs = initializer - else: - self.tibs = Mutibs() + def __init__(self, initializer: Mutibs) -> None: + self.tibs = initializer @classmethod def from_zeros(cls, i: int): @@ -203,13 +191,6 @@ def from_zeros(cls, i: int): x.tibs = Mutibs.from_zeros(i) return x - @classmethod - def from_mutibs(cls, mb: Mutibs): - assert isinstance(mb, Mutibs) - x = super().__new__(cls) - x.tibs = mb - return x - @classmethod def from_bytes(cls, b: Union[bytes, bytearray, memoryview], /) -> MutableBitStore: x = super().__new__(cls) @@ -273,16 +254,16 @@ def __eq__(self, other: Any, /) -> bool: return self.tibs == other.tibs def __and__(self, other: MutableBitStore, /) -> MutableBitStore: - return MutableBitStore.from_mutibs(self.tibs & other.tibs) + return MutableBitStore(self.tibs & other.tibs) def __or__(self, other: MutableBitStore, /) -> MutableBitStore: - return MutableBitStore.from_mutibs(self.tibs | other.tibs) + return MutableBitStore(self.tibs | other.tibs) def __xor__(self, other: MutableBitStore, /) -> MutableBitStore: - return MutableBitStore.from_mutibs(self.tibs ^ other.tibs) + return MutableBitStore(self.tibs ^ other.tibs) def __invert__(self) -> MutableBitStore: - return MutableBitStore.from_mutibs(~self.tibs) + return MutableBitStore(~self.tibs) def find(self, bs: MutableBitStore, start: int, end: int, bytealigned: bool = False) -> int: assert start >= 0 @@ -317,7 +298,7 @@ def extend_left(self, other: MutableBitStore, /) -> None: def _mutable_copy(self) -> MutableBitStore: """Always creates a copy, even if instance is immutable.""" - return MutableBitStore.from_mutibs(self.tibs.__copy__()) + return MutableBitStore(self.tibs.__copy__()) def copy(self) -> MutableBitStore: return self._mutable_copy() From b314d7553323df251de4c0c8053ce41b47b9eeb9 Mon Sep 17 00:00:00 2001 From: Scott Griffiths Date: Sat, 14 Mar 2026 09:40:55 +0000 Subject: [PATCH 08/35] A few simplifications. --- bitstring/bitstore_tibs.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/bitstring/bitstore_tibs.py b/bitstring/bitstore_tibs.py index 5e465bf1..55a3d483 100644 --- a/bitstring/bitstore_tibs.py +++ b/bitstring/bitstore_tibs.py @@ -95,8 +95,7 @@ def to_oct(self) -> str: return self.tibs.to_oct() def __add__(self, other: ConstBitStore, /) -> ConstBitStore: - newbits = self.tibs + other.tibs - return ConstBitStore(newbits) + return ConstBitStore(self.tibs + other.tibs) def __eq__(self, other: Any, /) -> bool: return self.tibs == other.tibs @@ -141,7 +140,7 @@ def _mutable_copy(self) -> MutableBitStore: return MutableBitStore(self.tibs.to_mutibs()) def copy(self) -> ConstBitStore: - return self if isinstance(self.tibs, Tibs) else self._mutable_copy() + return self def __getitem__(self, item: Union[int, slice], /) -> Union[int, ConstBitStore]: # Use getindex or getslice instead @@ -246,9 +245,7 @@ def __irshift__(self, n: int, /) -> None: self.tibs >>= n def __add__(self, other: MutableBitStore, /) -> MutableBitStore: - bs = self._mutable_copy() - bs.tibs += other.tibs - return bs + return MutableBitStore(self.tibs + other.tibs) def __eq__(self, other: Any, /) -> bool: return self.tibs == other.tibs From 59f5dd6074a72652a1d2f41aafcefa422c0049b2 Mon Sep 17 00:00:00 2001 From: Scott Griffiths Date: Sat, 14 Mar 2026 13:19:38 +0000 Subject: [PATCH 09/35] Defaulting more to Mutibs during construction. --- bitstring/bits.py | 2 +- bitstring/bitstore_helpers.py | 96 +++++++++++++++++------------------ bitstring/bitstore_tibs.py | 21 +++++++- 3 files changed, 66 insertions(+), 53 deletions(-) diff --git a/bitstring/bits.py b/bitstring/bits.py index f0faa4a6..5daf3fa2 100644 --- a/bitstring/bits.py +++ b/bitstring/bits.py @@ -614,7 +614,7 @@ def _setbytes_with_truncation(self, data: Union[bytearray, bytes], length: Optio else: if length + offset > len(data) * 8: raise bitstring.CreationError(f"Not enough data present. Need {length + offset} bits, have {len(data) * 8}.") - self._bitstore = ConstBitStore.from_bytes(data).getslice_msb0(offset, offset + length) + self._bitstore = MutableBitStore.from_bytes(data).getslice_msb0(offset, offset + length) def _getbytes(self) -> bytes: """Return the data as an ordinary bytes object.""" diff --git a/bitstring/bitstore_helpers.py b/bitstring/bitstore_helpers.py index 4a4eb4b9..a5bbb10e 100644 --- a/bitstring/bitstore_helpers.py +++ b/bitstring/bitstore_helpers.py @@ -1,9 +1,6 @@ from __future__ import annotations -from typing import Union -from tibs import Tibs, Mutibs -import bitstring - +from tibs import Mutibs import struct import math @@ -16,52 +13,51 @@ MutableBitStore = bitstring.bitstore.MutableBitStore -ConstBitStore = bitstring.bitstore.ConstBitStore from bitstring.helpers import tidy_input_string -def bin2bitstore(binstring: str) -> ConstBitStore: +def bin2bitstore(binstring: str) -> MutableBitStore: binstring = tidy_input_string(binstring) binstring = binstring.replace('0b', '') - mb = Tibs.from_bin(binstring) - return ConstBitStore(mb) + mb = Mutibs.from_bin(binstring) + return MutableBitStore(mb) -def hex2bitstore(hexstring: str) -> ConstBitStore: +def hex2bitstore(hexstring: str) -> MutableBitStore: hexstring = tidy_input_string(hexstring) hexstring = hexstring.replace('0x', '') - mb = Tibs.from_hex(hexstring) - return ConstBitStore(mb) + mb = Mutibs.from_hex(hexstring) + return MutableBitStore(mb) -def oct2bitstore(octstring: str) -> ConstBitStore: +def oct2bitstore(octstring: str) -> MutableBitStore: octstring = tidy_input_string(octstring) octstring = octstring.replace('0o', '') - mb = Tibs.from_oct(octstring) - return ConstBitStore(mb) + mb = Mutibs.from_oct(octstring) + return MutableBitStore(mb) -def int2bitstore(i: int, length: int, signed: bool) -> ConstBitStore: +def int2bitstore(i: int, length: int, signed: bool) -> MutableBitStore: i = int(i) if length <= 128: try: if signed: - mb = Tibs.from_i(i, length=length) + mb = Mutibs.from_i(i, length=length) else: - mb = Tibs.from_u(i, length=length) + mb = Mutibs.from_u(i, length=length) except OverflowError as e: raise ValueError(e) else: b = i.to_bytes((length + 7) // 8, byteorder="big", signed=signed) offset = 8 - (length % 8) - mb = Tibs.from_bytes(b) + mb = Mutibs.from_bytes(b) if offset != 8: mb = mb[offset:] - return ConstBitStore(mb) + return MutableBitStore(mb) -def intle2bitstore(i: int, length: int, signed: bool) -> ConstBitStore: +def intle2bitstore(i: int, length: int, signed: bool) -> MutableBitStore: i = int(i) if length <= 128: try: @@ -78,27 +74,27 @@ def intle2bitstore(i: int, length: int, signed: bool) -> ConstBitStore: mb = Mutibs.from_bytes(b) if offset != 8: mb = mb[offset:] - return ConstBitStore(mb.as_tibs()) + return MutableBitStore(mb) -def float2bitstore(f: Union[str, float], length: int, big_endian: bool) -> ConstBitStore: +def float2bitstore(f: Union[str, float], length: int, big_endian: bool) -> MutableBitStore: f = float(f) mb = Mutibs.from_f(f, length) if not big_endian: mb.byte_swap() - return ConstBitStore(mb.as_tibs()) + return MutableBitStore(mb) CACHE_SIZE = 256 @functools.lru_cache(CACHE_SIZE) -def str_to_bitstore(s: str) -> ConstBitStore: +def str_to_bitstore(s: str) -> MutableBitStore: _, tokens = bitstring.utils.tokenparser(s) constbitstores = [bitstore_from_token(*token) for token in tokens] - return ConstBitStore.join(constbitstores) + return MutableBitStore.join(constbitstores) -literal_bit_funcs: Dict[str, Callable[..., ConstBitStore]] = { +literal_bit_funcs: Dict[str, Callable[..., MutableBitStore]] = { '0x': hex2bitstore, '0X': hex2bitstore, '0b': bin2bitstore, @@ -108,7 +104,7 @@ def str_to_bitstore(s: str) -> ConstBitStore: } -def bitstore_from_token(name: str, token_length: Optional[int], value: Optional[str]) -> ConstBitStore: +def bitstore_from_token(name: str, token_length: Optional[int], value: Optional[str]) -> MutableBitStore: if name in literal_bit_funcs: return literal_bit_funcs[name](value) try: @@ -125,22 +121,22 @@ def bitstore_from_token(name: str, token_length: Optional[int], value: Optional[ -def ue2bitstore(i: Union[str, int]) -> ConstBitStore: +def ue2bitstore(i: Union[str, int]) -> MutableBitStore: i = int(i) if i < 0: raise bitstring.CreationError("Cannot use negative initialiser for unsigned exponential-Golomb.") if i == 0: - return ConstBitStore.from_bin('1') + return MutableBitStore.from_bin('1') tmp = i + 1 leadingzeros = -1 while tmp > 0: tmp >>= 1 leadingzeros += 1 remainingpart = i + 1 - (1 << leadingzeros) - return ConstBitStore.from_bin('0' * leadingzeros + '1') + int2bitstore(remainingpart, leadingzeros, False) + return MutableBitStore.from_bin('0' * leadingzeros + '1') + int2bitstore(remainingpart, leadingzeros, False) -def se2bitstore(i: Union[str, int]) -> ConstBitStore: +def se2bitstore(i: Union[str, int]) -> MutableBitStore: i = int(i) if i > 0: u = (i * 2) - 1 @@ -149,22 +145,22 @@ def se2bitstore(i: Union[str, int]) -> ConstBitStore: return ue2bitstore(u) -def uie2bitstore(i: Union[str, int]) -> ConstBitStore: +def uie2bitstore(i: Union[str, int]) -> MutableBitStore: i = int(i) if i < 0: raise bitstring.CreationError("Cannot use negative initialiser for unsigned interleaved exponential-Golomb.") - return ConstBitStore.from_bin('1' if i == 0 else '0' + '0'.join(bin(i + 1)[3:]) + '1') + return MutableBitStore.from_bin('1' if i == 0 else '0' + '0'.join(bin(i + 1)[3:]) + '1') -def sie2bitstore(i: Union[str, int]) -> ConstBitStore: +def sie2bitstore(i: Union[str, int]) -> MutableBitStore: i = int(i) if i == 0: - return ConstBitStore.from_bin('1') + return MutableBitStore.from_bin('1') else: - return uie2bitstore(abs(i)) + (ConstBitStore.from_bin('1') if i < 0 else ConstBitStore.from_bin('0')) + return uie2bitstore(abs(i)) + (MutableBitStore.from_bin('1') if i < 0 else MutableBitStore.from_bin('0')) -def bfloat2bitstore(f: Union[str, float], big_endian: bool) -> ConstBitStore: +def bfloat2bitstore(f: Union[str, float], big_endian: bool) -> MutableBitStore: f = float(f) fmt = '>f' if big_endian else ' ConstBitStore: except OverflowError: # For consistency, we overflow to 'inf'. b = struct.pack(fmt, float('inf') if f > 0 else float('-inf')) - return ConstBitStore.from_bytes(b[0:2]) if big_endian else ConstBitStore.from_bytes(b[2:4]) + return MutableBitStore.from_bytes(b[0:2]) if big_endian else MutableBitStore.from_bytes(b[2:4]) -def p4binary2bitstore(f: Union[str, float]) -> ConstBitStore: +def p4binary2bitstore(f: Union[str, float]) -> MutableBitStore: f = float(f) u = p4binary_fmt.float_to_int8(f) return int2bitstore(u, 8, False) -def p3binary2bitstore(f: Union[str, float]) -> ConstBitStore: +def p3binary2bitstore(f: Union[str, float]) -> MutableBitStore: f = float(f) u = p3binary_fmt.float_to_int8(f) return int2bitstore(u, 8, False) -def e4m3mxfp2bitstore(f: Union[str, float]) -> ConstBitStore: +def e4m3mxfp2bitstore(f: Union[str, float]) -> MutableBitStore: f = float(f) if bitstring.options.mxfp_overflow == 'saturate': u = e4m3mxfp_saturate_fmt.float_to_int(f) @@ -196,7 +192,7 @@ def e4m3mxfp2bitstore(f: Union[str, float]) -> ConstBitStore: return int2bitstore(u, 8, False) -def e5m2mxfp2bitstore(f: Union[str, float]) -> ConstBitStore: +def e5m2mxfp2bitstore(f: Union[str, float]) -> MutableBitStore: f = float(f) if bitstring.options.mxfp_overflow == 'saturate': u = e5m2mxfp_saturate_fmt.float_to_int(f) @@ -205,7 +201,7 @@ def e5m2mxfp2bitstore(f: Union[str, float]) -> ConstBitStore: return int2bitstore(u, 8, False) -def e3m2mxfp2bitstore(f: Union[str, float]) -> ConstBitStore: +def e3m2mxfp2bitstore(f: Union[str, float]) -> MutableBitStore: f = float(f) if math.isnan(f): raise ValueError("Cannot convert float('nan') to e3m2mxfp format as it has no representation for it.") @@ -213,7 +209,7 @@ def e3m2mxfp2bitstore(f: Union[str, float]) -> ConstBitStore: return int2bitstore(u, 6, False) -def e2m3mxfp2bitstore(f: Union[str, float]) -> ConstBitStore: +def e2m3mxfp2bitstore(f: Union[str, float]) -> MutableBitStore: f = float(f) if math.isnan(f): raise ValueError("Cannot convert float('nan') to e2m3mxfp format as it has no representation for it.") @@ -221,7 +217,7 @@ def e2m3mxfp2bitstore(f: Union[str, float]) -> ConstBitStore: return int2bitstore(u, 6, False) -def e2m1mxfp2bitstore(f: Union[str, float]) -> ConstBitStore: +def e2m1mxfp2bitstore(f: Union[str, float]) -> MutableBitStore: f = float(f) if math.isnan(f): raise ValueError("Cannot convert float('nan') to e2m1mxfp format as it has no representation for it.") @@ -232,10 +228,10 @@ def e2m1mxfp2bitstore(f: Union[str, float]) -> ConstBitStore: e8m0mxfp_allowed_values = [float(2 ** x) for x in range(-127, 128)] -def e8m0mxfp2bitstore(f: Union[str, float]) -> ConstBitStore: +def e8m0mxfp2bitstore(f: Union[str, float]) -> MutableBitStore: f = float(f) if math.isnan(f): - return ConstBitStore.from_bin('11111111') + return MutableBitStore.from_bin('11111111') try: i = e8m0mxfp_allowed_values.index(f) except ValueError: @@ -243,15 +239,15 @@ def e8m0mxfp2bitstore(f: Union[str, float]) -> ConstBitStore: return int2bitstore(i, 8, False) -def mxint2bitstore(f: Union[str, float]) -> ConstBitStore: +def mxint2bitstore(f: Union[str, float]) -> MutableBitStore: f = float(f) if math.isnan(f): raise ValueError("Cannot convert float('nan') to mxint format as it has no representation for it.") f *= 2 ** 6 # Remove the implicit scaling factor if f > 127: # 1 + 63/64 - return ConstBitStore.from_bin('01111111') + return MutableBitStore.from_bin('01111111') if f <= -128: # -2 - return ConstBitStore.from_bin('10000000') + return MutableBitStore.from_bin('10000000') # Want to round to nearest, so move by 0.5 away from zero and round down by converting to int if f >= 0.0: f += 0.5 diff --git a/bitstring/bitstore_tibs.py b/bitstring/bitstore_tibs.py index 55a3d483..f6b8b87c 100644 --- a/bitstring/bitstore_tibs.py +++ b/bitstring/bitstore_tibs.py @@ -184,6 +184,12 @@ class MutableBitStore: def __init__(self, initializer: Mutibs) -> None: self.tibs = initializer + @classmethod + def join(cls, bitstores: Iterable[MutableBitStore], /) -> MutableBitStore: + x = super().__new__(cls) + x.tibs = Mutibs.from_joined(b.tibs for b in bitstores) + return x + @classmethod def from_zeros(cls, i: int): x = super().__new__(cls) @@ -215,7 +221,12 @@ def to_bytes(self, pad_at_end: bool = True) -> bytes: def to_u(self) -> int: if len(self) > 128: - return int.from_bytes(self.to_bytes(pad_at_end=False), byteorder="big", signed=False) + padding = 8 - len(self.tibs) % 8 + if padding == 8: + b = self.tibs.to_bytes() + else: + b = ([0] * padding + self.tibs).to_bytes() + return int.from_bytes(b, byteorder="big", signed=False) try: return self.tibs.to_u() except OverflowError as e: @@ -223,7 +234,13 @@ def to_u(self) -> int: def to_i(self) -> int: if len(self) > 128: - return int.from_bytes(self.to_bytes(pad_at_end=False), byteorder="big", signed=True) + padding = 8 - len(self.tibs) % 8 + if padding == 8: + b = self.tibs.to_bytes() + else: + pad_bit = self.tibs[0] # Keep sign when padding + b = ([pad_bit] * padding + self.tibs).to_bytes() + return int.from_bytes(b, byteorder="big", signed=True) try: return self.tibs.to_i() except OverflowError as e: From 0efbd2969f2f5335741bc3fcd1e3a020c85799d2 Mon Sep 17 00:00:00 2001 From: Scott Griffiths Date: Fri, 3 Apr 2026 19:17:46 +0100 Subject: [PATCH 10/35] Fixes for upgrade to tibs 0.6. --- bitstring/__init__.py | 1 - bitstring/bitstore_helpers.py | 11 ++++------- bitstring/bitstore_tibs.py | 5 ++++- pyproject.toml | 2 +- 4 files changed, 9 insertions(+), 10 deletions(-) diff --git a/bitstring/__init__.py b/bitstring/__init__.py index 8fd5b92d..37f829d6 100644 --- a/bitstring/__init__.py +++ b/bitstring/__init__.py @@ -61,7 +61,6 @@ import sys import bitstring.bitstore_tibs as bitstore -# import bitstring.bitstore_helpers as bitstore_helpers from .bits import Bits from .bitstring_options import Options diff --git a/bitstring/bitstore_helpers.py b/bitstring/bitstore_helpers.py index a5bbb10e..f402a5c9 100644 --- a/bitstring/bitstore_helpers.py +++ b/bitstring/bitstore_helpers.py @@ -1,6 +1,6 @@ from __future__ import annotations -from tibs import Mutibs +from tibs import Mutibs, Endianness import struct import math @@ -62,12 +62,11 @@ def intle2bitstore(i: int, length: int, signed: bool) -> MutableBitStore: if length <= 128: try: if signed: - mb = Mutibs.from_i(i, length=length) + mb = Mutibs.from_i(i, length, Endianness.Little) else: - mb = Mutibs.from_u(i, length=length) + mb = Mutibs.from_u(i, length, Endianness.Little) except OverflowError as e: raise ValueError(e) - mb.byte_swap() else: b = i.to_bytes((length + 7) // 8, byteorder="little", signed=signed) offset = 8 - (length % 8) @@ -79,9 +78,7 @@ def intle2bitstore(i: int, length: int, signed: bool) -> MutableBitStore: def float2bitstore(f: Union[str, float], length: int, big_endian: bool) -> MutableBitStore: f = float(f) - mb = Mutibs.from_f(f, length) - if not big_endian: - mb.byte_swap() + mb = Mutibs.from_f(f, length, Endianness.Big if big_endian else Endianness.Little) return MutableBitStore(mb) diff --git a/bitstring/bitstore_tibs.py b/bitstring/bitstore_tibs.py index f6b8b87c..98593c0e 100644 --- a/bitstring/bitstore_tibs.py +++ b/bitstring/bitstore_tibs.py @@ -390,7 +390,10 @@ def setitem_msb0(self, key, value, /): else: if isinstance(key, slice): key = range(*key.indices(len(self))) - self.tibs.set(value, key) + if value: + self.tibs.set(key) + else: + self.tibs.unset(key) def delitem_msb0(self, key, /): self.tibs.__delitem__(key) diff --git a/pyproject.toml b/pyproject.toml index bd2b0deb..81fb9a4e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,7 +29,7 @@ classifiers = [ ] keywords = ["binary", "bitarray", "bitvector", "bitfield"] dependencies = [ - "tibs >= 0.5.6, < 0.6", + "tibs >= 0.6.0, < 0.7", ] [project.urls] From 6812800528079b85acb48a37bf3847f19352a7d5 Mon Sep 17 00:00:00 2001 From: Scott Griffiths Date: Fri, 3 Apr 2026 22:25:06 +0100 Subject: [PATCH 11/35] Removing some validation that's not needed any more as tibs is doing it. --- bitstring/bitarray_.py | 1 - bitstring/bits.py | 15 +++------------ bitstring/bitstore_tibs.py | 2 -- 3 files changed, 3 insertions(+), 15 deletions(-) diff --git a/bitstring/bitarray_.py b/bitstring/bitarray_.py index ff6d6a18..d9130c80 100644 --- a/bitstring/bitarray_.py +++ b/bitstring/bitarray_.py @@ -347,7 +347,6 @@ def replace(self, old: BitsType, new: BitsType, start: Optional[int] = None, end new = self._create_from_bitstype(new) if len(old) == 0: raise ValueError("Empty bitstring cannot be replaced.") - start, end = self._validate_slice(start, end) if new is self: # Prevent self assignment woes diff --git a/bitstring/bits.py b/bitstring/bits.py index 5daf3fa2..b4e83e6f 100644 --- a/bitstring/bits.py +++ b/bitstring/bits.py @@ -1182,19 +1182,16 @@ def find(self, bs: BitsType, /, start: Optional[int] = None, end: Optional[int] bs = Bits._create_from_bitstype(bs) if len(bs) == 0: raise ValueError("Cannot find an empty bitstring.") - start, end = self._validate_slice(start, end) ba = bitstring.options.bytealigned if bytealigned is None else bytealigned p = self._find(bs, start, end, ba) return p def _find_lsb0(self, bs: Bits, start: int, end: int, bytealigned: bool) -> Union[Tuple[int], Tuple[()]]: # A forward find in lsb0 is very like a reverse find in msb0. - assert start <= end assert bitstring.options.lsb0 new_slice = bitstring.helpers.offset_slice_indices_lsb0(slice(start, end, None), len(self)) - msb0_start, msb0_end = self._validate_slice(new_slice.start, new_slice.stop) - p = self._rfind_msb0(bs, msb0_start, msb0_end, bytealigned) + p = self._rfind_msb0(bs, new_slice.start, new_slice.stop, bytealigned) if p: return (len(self) - p[0] - len(bs),) @@ -1227,7 +1224,6 @@ def findall(self, bs: BitsType, start: Optional[int] = None, end: Optional[int] if count is not None and count < 0: raise ValueError("In findall, count must be >= 0.") bs = Bits._create_from_bitstype(bs) - start, end = self._validate_slice(start, end) ba = bitstring.options.bytealigned if bytealigned is None else bytealigned return self._findall(bs, start, end, count, ba) @@ -1243,11 +1239,10 @@ def _findall_msb0(self, bs: Bits, start: int, end: int, count: Optional[int], def _findall_lsb0(self, bs: Bits, start: int, end: int, count: Optional[int], bytealigned: bool) -> Iterable[int]: - assert start <= end assert bitstring.options.lsb0 new_slice = bitstring.helpers.offset_slice_indices_lsb0(slice(start, end, None), len(self)) - msb0_start, msb0_end = self._validate_slice(new_slice.start, new_slice.stop) + msb0_start, msb0_end = new_slice.start, new_slice.stop # Search chunks starting near the end and then moving back. c = 0 @@ -1293,10 +1288,7 @@ def rfind(self, bs: BitsType, /, start: Optional[int] = None, end: Optional[int] """ bs = Bits._create_from_bitstype(bs) - start, end = self._validate_slice(start, end) ba = bitstring.options.bytealigned if bytealigned is None else bytealigned - if len(bs) == 0: - raise ValueError("Cannot find an empty bitstring.") p = self._rfind(bs, start, end, ba) return p @@ -1307,10 +1299,9 @@ def _rfind_msb0(self, bs: Bits, start: int, end: int, bytealigned: bool) -> Unio def _rfind_lsb0(self, bs: Bits, start: int, end: int, bytealigned: bool) -> Union[Tuple[int], Tuple[()]]: # A reverse find in lsb0 is very like a forward find in msb0. - assert start <= end assert bitstring.options.lsb0 new_slice = bitstring.helpers.offset_slice_indices_lsb0(slice(start, end, None), len(self)) - msb0_start, msb0_end = self._validate_slice(new_slice.start, new_slice.stop) + msb0_start, msb0_end = new_slice.start, new_slice.stop p = self._find_msb0(bs, msb0_start, msb0_end, bytealigned) if p: diff --git a/bitstring/bitstore_tibs.py b/bitstring/bitstore_tibs.py index 98593c0e..a6fc0ccf 100644 --- a/bitstring/bitstore_tibs.py +++ b/bitstring/bitstore_tibs.py @@ -280,11 +280,9 @@ def __invert__(self) -> MutableBitStore: return MutableBitStore(~self.tibs) def find(self, bs: MutableBitStore, start: int, end: int, bytealigned: bool = False) -> int: - assert start >= 0 return self.tibs.find(bs.tibs, start, end, byte_aligned=bytealigned) def rfind(self, bs: MutableBitStore, start: int, end: int, bytealigned: bool = False): - assert start >= 0 return self.tibs.rfind(bs.tibs, start, end, byte_aligned=bytealigned) def findall_msb0(self, bs: MutableBitStore, start: int, end: int, bytealigned: bool = False) -> Iterator[int]: From fbc0236c13c45e5d084010aaf1efee966b4589fb Mon Sep 17 00:00:00 2001 From: Scott Griffiths Date: Sat, 4 Apr 2026 07:38:27 +0100 Subject: [PATCH 12/35] Better encapsulation of tibs and using more of its methods. --- bitstring/bitarray_.py | 6 +- bitstring/bits.py | 12 ++- bitstring/bitstore_tibs.py | 148 +++++++++++++++++++++++-------------- 3 files changed, 101 insertions(+), 65 deletions(-) diff --git a/bitstring/bitarray_.py b/bitstring/bitarray_.py index d9130c80..522c136d 100644 --- a/bitstring/bitarray_.py +++ b/bitstring/bitarray_.py @@ -276,17 +276,17 @@ def __imul__(self: TBits, n: int) -> TBits: def __ior__(self: TBits, bs: BitsType) -> TBits: bs = self._create_from_bitstype(bs) - self._bitstore.tibs |= bs._bitstore.tibs + self._bitstore |= bs._bitstore return self def __iand__(self: TBits, bs: BitsType) -> TBits: bs = self._create_from_bitstype(bs) - self._bitstore.tibs &= bs._bitstore.tibs + self._bitstore &= bs._bitstore return self def __ixor__(self: TBits, bs: BitsType) -> TBits: bs = self._create_from_bitstype(bs) - self._bitstore.tibs ^= bs._bitstore.tibs + self._bitstore ^= bs._bitstore return self def _replace(self, old: Bits, new: Bits, start: int, end: int, count: int, bytealigned: Optional[bool]) -> int: diff --git a/bitstring/bits.py b/bitstring/bits.py index b4e83e6f..6dfda9bd 100644 --- a/bitstring/bits.py +++ b/bitstring/bits.py @@ -498,7 +498,7 @@ def _setauto_no_length_or_offset(self, s: BitsType, /) -> None: elif isinstance(s, Bits): self._bitstore = s._bitstore.copy() elif isinstance(s, (bytes, bytearray, memoryview)): - self._bitstore = ConstBitStore.from_bytes(bytearray(s)) + self._bitstore = ConstBitStore.from_bytes(s) elif isinstance(s, io.BytesIO): self._bitstore = ConstBitStore.from_bytes(s.getvalue()) elif isinstance(s, io.BufferedReader): @@ -506,8 +506,7 @@ def _setauto_no_length_or_offset(self, s: BitsType, /) -> None: elif isinstance(s, array.array): self._bitstore = ConstBitStore.from_bytes(s.tobytes()) elif isinstance(s, abc.Iterable): - # Evaluate each item as True or False and set bits to 1 or 0. - self._setbin(''.join(str(int(bool(x))) for x in s)) + self._bitstore = ConstBitStore.from_bools(s) elif isinstance(s, numbers.Integral): raise TypeError(f"It's no longer possible to auto initialise a bitstring from an integer." f" Use '{self.__class__.__name__}({s})' instead of just '{s}' as this makes it " @@ -605,7 +604,6 @@ def _setbytes_with_truncation(self, data: Union[bytearray, bytes], length: Optio """Set the data from a bytes or bytearray object, with optional offset and length truncations.""" if offset is None and length is None: return self._setbytes(data) - data = bytearray(data) if offset is None: offset = 0 if length is None: @@ -614,7 +612,7 @@ def _setbytes_with_truncation(self, data: Union[bytearray, bytes], length: Optio else: if length + offset > len(data) * 8: raise bitstring.CreationError(f"Not enough data present. Need {length + offset} bits, have {len(data) * 8}.") - self._bitstore = MutableBitStore.from_bytes(data).getslice_msb0(offset, offset + length) + self._bitstore = ConstBitStore.from_bytes(data).getslice_msb0(offset, offset + length) def _getbytes(self) -> bytes: """Return the data as an ordinary bytes object.""" @@ -1023,7 +1021,7 @@ def _readtoken(self, name: str, pos: int, length: Optional[int]) -> Tuple[Union[ def _addright(self, bs: Bits, /) -> None: """Add a bitstring to the RHS of the current bitstring.""" - self._bitstore.tibs += bs._bitstore.tibs + self._bitstore += bs._bitstore def _addleft(self, bs: Bits, /) -> None: """Prepend a bitstring to the current bitstring.""" @@ -1500,7 +1498,7 @@ def count(self, value: Any) -> int: 7 """ - return self._bitstore.tibs.count(bool(value)) + return self._bitstore.count(bool(value)) @staticmethod def _format_bits(bits: Bits, bits_per_group: int, sep: str, dtype: Dtype, diff --git a/bitstring/bitstore_tibs.py b/bitstring/bitstore_tibs.py index a6fc0ccf..6a1dd272 100644 --- a/bitstring/bitstore_tibs.py +++ b/bitstring/bitstore_tibs.py @@ -7,6 +7,27 @@ from bitstring.helpers import offset_slice_indices_lsb0 +def _fallback_to_u(tibs: Tibs | Mutibs) -> int: + if len(tibs) == 0: + # Keep tibs' validation behaviour for zero-length conversion. + return tibs.to_u() + padding = (-len(tibs)) % 8 + if padding: + tibs = [0] * padding + tibs + return int.from_bytes(tibs.to_bytes(), byteorder="big", signed=False) + + +def _fallback_to_i(tibs: Tibs | Mutibs) -> int: + if len(tibs) == 0: + # Keep tibs' validation behaviour for zero-length conversion. + return tibs.to_i() + padding = (-len(tibs)) % 8 + if padding: + pad_bit = tibs[0] + tibs = [pad_bit] * padding + tibs + return int.from_bytes(tibs.to_bytes(), byteorder="big", signed=True) + + class ConstBitStore: """A light wrapper around tibs.Tibs that does the LSB0 stuff""" @@ -33,11 +54,17 @@ def from_bytes(cls, b: Union[bytes, bytearray, memoryview], /) -> ConstBitStore: x.tibs = Tibs.from_bytes(b) return x + @classmethod + def from_bools(cls, iterable: Iterable[Any], /) -> ConstBitStore: + x = super().__new__(cls) + x.tibs = Tibs.from_bools(iterable) + return x + @classmethod def frombuffer(cls, buffer, /, length: Optional[int] = None) -> ConstBitStore: x = super().__new__(cls) - # TODO: tibs needs a Tibs.from_buffer method. - x.tibs = Tibs.from_bytes(bytes(buffer)) + # from_bytes accepts memoryview, so avoid an eager bytes copy. + x.tibs = Tibs.from_bytes(memoryview(buffer)) if length is not None: if length < 0: raise CreationError("Can't create bitstring with a negative length.") @@ -59,31 +86,16 @@ def to_bytes(self) -> bytes: return (self.tibs + [0] * padding).to_bytes() def to_u(self) -> int: - if len(self) > 128: - padding = 8 - len(self.tibs) % 8 - if padding == 8: - b = self.tibs.to_bytes() - else: - b = ([0] * padding + self.tibs).to_bytes() - return int.from_bytes(b, byteorder="big", signed=False) try: return self.tibs.to_u() - except OverflowError as e: - raise ValueError(e) + except ValueError: + return _fallback_to_u(self.tibs) def to_i(self) -> int: - if len(self) > 128: - padding = 8 - len(self.tibs) % 8 - if padding == 8: - b = self.tibs.to_bytes() - else: - pad_bit = self.tibs[0] # Keep sign when padding - b = ([pad_bit] * padding + self.tibs).to_bytes() - return int.from_bytes(b, byteorder="big", signed=True) try: return self.tibs.to_i() - except OverflowError as e: - raise ValueError(e) + except ValueError: + return _fallback_to_i(self.tibs) def to_hex(self) -> str: return self.tibs.to_hex() @@ -113,22 +125,16 @@ def __invert__(self) -> ConstBitStore: return ConstBitStore(~self.tibs) def find(self, bs: ConstBitStore, start: int, end: int, bytealigned: bool = False) -> int | None: - assert start >= 0 return self.tibs.find(bs.tibs, start, end, byte_aligned=bytealigned) def rfind(self, bs: ConstBitStore, start: int, end: int, bytealigned: bool = False) -> int | None: - assert start >= 0 return self.tibs.rfind(bs.tibs, start, end, byte_aligned=bytealigned) def findall_msb0(self, bs: ConstBitStore, start: int, end: int, bytealigned: bool = False) -> Iterator[int]: - x = self.tibs - for p in x.find_all(bs.tibs, start=start, end=end, byte_aligned=bytealigned): - yield p + return self.tibs.find_all(bs.tibs, start=start, end=end, byte_aligned=bytealigned) def rfindall_msb0(self, bs: ConstBitStore, start: int, end: int, bytealigned: bool = False) -> Iterator[int]: - x = self.tibs - for p in x.rfind_all(bs.tibs, start=start, end=end, byte_aligned=bytealigned): - yield p + return self.tibs.rfind_all(bs.tibs, start=start, end=end, byte_aligned=bytealigned) def __iter__(self) -> Iterable[bool]: length = len(self) @@ -172,6 +178,15 @@ def any(self) -> bool: def all(self) -> bool: return self.tibs.all() + def startswith(self, prefix: ConstBitStore) -> bool: + return self.tibs.starts_with(prefix.tibs) + + def endswith(self, suffix: ConstBitStore) -> bool: + return self.tibs.ends_with(suffix.tibs) + + def count(self, value: Any) -> int: + return self.tibs.count(value) + def __len__(self) -> int: return len(self.tibs) @@ -202,6 +217,12 @@ def from_bytes(cls, b: Union[bytes, bytearray, memoryview], /) -> MutableBitStor x.tibs = Mutibs.from_bytes(b) return x + @classmethod + def from_bools(cls, iterable: Iterable[Any], /) -> MutableBitStore: + x = super().__new__(cls) + x.tibs = Mutibs.from_bools(iterable) + return x + @classmethod def from_bin(cls, s: str) -> MutableBitStore: x = super().__new__(cls) @@ -220,31 +241,16 @@ def to_bytes(self, pad_at_end: bool = True) -> bytes: return self.tibs.to_bytes() def to_u(self) -> int: - if len(self) > 128: - padding = 8 - len(self.tibs) % 8 - if padding == 8: - b = self.tibs.to_bytes() - else: - b = ([0] * padding + self.tibs).to_bytes() - return int.from_bytes(b, byteorder="big", signed=False) try: return self.tibs.to_u() - except OverflowError as e: - raise ValueError(e) + except ValueError: + return _fallback_to_u(self.tibs) def to_i(self) -> int: - if len(self) > 128: - padding = 8 - len(self.tibs) % 8 - if padding == 8: - b = self.tibs.to_bytes() - else: - pad_bit = self.tibs[0] # Keep sign when padding - b = ([pad_bit] * padding + self.tibs).to_bytes() - return int.from_bytes(b, byteorder="big", signed=True) try: return self.tibs.to_i() - except OverflowError as e: - raise ValueError(e) + except ValueError: + return _fallback_to_i(self.tibs) def to_hex(self) -> str: return self.tibs.to_hex() @@ -264,18 +270,34 @@ def __irshift__(self, n: int, /) -> None: def __add__(self, other: MutableBitStore, /) -> MutableBitStore: return MutableBitStore(self.tibs + other.tibs) + def __iadd__(self, other: MutableBitStore | ConstBitStore, /) -> MutableBitStore: + self.tibs += other.tibs + return self + def __eq__(self, other: Any, /) -> bool: return self.tibs == other.tibs def __and__(self, other: MutableBitStore, /) -> MutableBitStore: return MutableBitStore(self.tibs & other.tibs) + def __iand__(self, other: MutableBitStore | ConstBitStore, /) -> MutableBitStore: + self.tibs &= other.tibs + return self + def __or__(self, other: MutableBitStore, /) -> MutableBitStore: return MutableBitStore(self.tibs | other.tibs) + def __ior__(self, other: MutableBitStore | ConstBitStore, /) -> MutableBitStore: + self.tibs |= other.tibs + return self + def __xor__(self, other: MutableBitStore, /) -> MutableBitStore: return MutableBitStore(self.tibs ^ other.tibs) + def __ixor__(self, other: MutableBitStore | ConstBitStore, /) -> MutableBitStore: + self.tibs ^= other.tibs + return self + def __invert__(self) -> MutableBitStore: return MutableBitStore(~self.tibs) @@ -286,14 +308,10 @@ def rfind(self, bs: MutableBitStore, start: int, end: int, bytealigned: bool = F return self.tibs.rfind(bs.tibs, start, end, byte_aligned=bytealigned) def findall_msb0(self, bs: MutableBitStore, start: int, end: int, bytealigned: bool = False) -> Iterator[int]: - x = self.tibs.to_tibs() - for p in x.find_all(bs.tibs, start=start, end=end, byte_aligned=bytealigned): - yield p + return self.tibs.to_tibs().find_all(bs.tibs, start=start, end=end, byte_aligned=bytealigned) def rfindall_msb0(self, bs: MutableBitStore, start: int, end: int, bytealigned: bool = False) -> Iterator[int]: - x = self.tibs.to_tibs() - for p in x.rfind_all(bs.tibs, start=start, end=end, byte_aligned=bytealigned): - yield p + return self.tibs.to_tibs().rfind_all(bs.tibs, start=start, end=end, byte_aligned=bytealigned) def clear(self) -> None: self.tibs.clear() @@ -305,7 +323,7 @@ def __iter__(self) -> Iterable[bool]: for i in range(len(self)): yield self.getindex(i) - def extend_left(self, other: MutableBitStore, /) -> None: + def extend_left(self, other: MutableBitStore | ConstBitStore, /) -> None: self.tibs.extend_left(other.tibs) def _mutable_copy(self) -> MutableBitStore: @@ -379,6 +397,26 @@ def any(self) -> bool: def all(self) -> bool: return self.tibs.all() + def startswith(self, prefix: MutableBitStore | ConstBitStore) -> bool: + return self.tibs.starts_with(prefix.tibs) + + def endswith(self, suffix: MutableBitStore | ConstBitStore) -> bool: + return self.tibs.ends_with(suffix.tibs) + + def count(self, value: Any) -> int: + return self.tibs.count(value) + + def replace(self, old: MutableBitStore | ConstBitStore, new: MutableBitStore | ConstBitStore, + start: Optional[int] = None, end: Optional[int] = None, + count: Optional[int] = None, bytealigned: bool = False) -> None: + self.tibs.replace(old.tibs, new.tibs, start=start, end=end, count=count, byte_aligned=bytealigned) + + def rotate_left(self, n: int, start: Optional[int] = None, end: Optional[int] = None) -> None: + self.tibs.rotate_left(n, start=start, end=end) + + def rotate_right(self, n: int, start: Optional[int] = None, end: Optional[int] = None) -> None: + self.tibs.rotate_right(n, start=start, end=end) + def __len__(self) -> int: return len(self.tibs) From 362f91cce03e28c632d4f32547c33590036d6d9c Mon Sep 17 00:00:00 2001 From: Scott Griffiths Date: Sat, 4 Apr 2026 07:40:12 +0100 Subject: [PATCH 13/35] Removing duplicated length check. --- bitstring/methods.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/bitstring/methods.py b/bitstring/methods.py index c9de3868..08f247b8 100644 --- a/bitstring/methods.py +++ b/bitstring/methods.py @@ -54,7 +54,7 @@ def pack(fmt: Union[str, List[str]], *values, **kwargs) -> BitStream: except ValueError as e: raise CreationError(*e.args) value_iter = iter(values) - bsl: List[BitStore] = [] + bsl: List[MutableBitStore] = [] try: for name, length, value in tokens: # If the value is in the kwd dictionary then it takes precedence. @@ -70,12 +70,6 @@ def pack(fmt: Union[str, List[str]], *values, **kwargs) -> BitStream: if value is None and name != 'pad': # Take the next value from the ones provided value = next(value_iter) - if name == 'bits': - value = bitstring.bits.Bits(value) - if length is not None and length != len(value): - raise CreationError(f"Token with length {length} packed with value of length {len(value)}.") - bsl.append(value._bitstore) - continue bsl.append(helpers.bitstore_from_token(name, length, value)) except StopIteration: raise CreationError(f"Not enough parameters present to pack according to the " From ebaff1ce5b8755aba559d3d0e7ae95995c69d48f Mon Sep 17 00:00:00 2001 From: Scott Griffiths Date: Sat, 4 Apr 2026 07:42:29 +0100 Subject: [PATCH 14/35] Factoring out mutibs creation from ints. --- bitstring/bitstore_helpers.py | 46 ++++++++++++++--------------------- 1 file changed, 18 insertions(+), 28 deletions(-) diff --git a/bitstring/bitstore_helpers.py b/bitstring/bitstore_helpers.py index f402a5c9..f89e0fdb 100644 --- a/bitstring/bitstore_helpers.py +++ b/bitstring/bitstore_helpers.py @@ -17,6 +17,22 @@ from bitstring.helpers import tidy_input_string +def _int_to_mutibs(i: int, length: int, signed: bool, little_endian: bool) -> Mutibs: + try: + if signed: + return Mutibs.from_i(i, length, Endianness.Little if little_endian else Endianness.Unspecified) + return Mutibs.from_u(i, length, Endianness.Little if little_endian else Endianness.Unspecified) + except (OverflowError, ValueError) as e: + # Keep tibs validation for normal sizes and unsupported values. + if length <= 128: + raise ValueError(e) + byteorder = "little" if little_endian else "big" + b = i.to_bytes((length + 7) // 8, byteorder=byteorder, signed=signed) + offset = (-length) % 8 + mb = Mutibs.from_bytes(b) + return mb if offset == 0 else mb[offset:] + + def bin2bitstore(binstring: str) -> MutableBitStore: binstring = tidy_input_string(binstring) binstring = binstring.replace('0b', '') @@ -40,39 +56,13 @@ def oct2bitstore(octstring: str) -> MutableBitStore: def int2bitstore(i: int, length: int, signed: bool) -> MutableBitStore: i = int(i) - if length <= 128: - try: - if signed: - mb = Mutibs.from_i(i, length=length) - else: - mb = Mutibs.from_u(i, length=length) - except OverflowError as e: - raise ValueError(e) - else: - b = i.to_bytes((length + 7) // 8, byteorder="big", signed=signed) - offset = 8 - (length % 8) - mb = Mutibs.from_bytes(b) - if offset != 8: - mb = mb[offset:] + mb = _int_to_mutibs(i, length, signed, little_endian=False) return MutableBitStore(mb) def intle2bitstore(i: int, length: int, signed: bool) -> MutableBitStore: i = int(i) - if length <= 128: - try: - if signed: - mb = Mutibs.from_i(i, length, Endianness.Little) - else: - mb = Mutibs.from_u(i, length, Endianness.Little) - except OverflowError as e: - raise ValueError(e) - else: - b = i.to_bytes((length + 7) // 8, byteorder="little", signed=signed) - offset = 8 - (length % 8) - mb = Mutibs.from_bytes(b) - if offset != 8: - mb = mb[offset:] + mb = _int_to_mutibs(i, length, signed, little_endian=True) return MutableBitStore(mb) From afa1b550c7545b41de2a518d88cdc9aa7c0aab56 Mon Sep 17 00:00:00 2001 From: Scott Griffiths Date: Sat, 4 Apr 2026 08:52:19 +0100 Subject: [PATCH 15/35] Using tibs.__iter__ directly. --- bitstring/bitstore_tibs.py | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/bitstring/bitstore_tibs.py b/bitstring/bitstore_tibs.py index 6a1dd272..d9562c0d 100644 --- a/bitstring/bitstore_tibs.py +++ b/bitstring/bitstore_tibs.py @@ -137,9 +137,7 @@ def rfindall_msb0(self, bs: ConstBitStore, start: int, end: int, bytealigned: bo return self.tibs.rfind_all(bs.tibs, start=start, end=end, byte_aligned=bytealigned) def __iter__(self) -> Iterable[bool]: - length = len(self) - for i in range(length): - yield self.getindex(i) + return self.tibs.__iter__() def _mutable_copy(self) -> MutableBitStore: """Always creates a copy, even if instance is immutable.""" @@ -153,14 +151,14 @@ def __getitem__(self, item: Union[int, slice], /) -> Union[int, ConstBitStore]: raise NotImplementedError def getindex_msb0(self, index: int, /) -> bool: - return self.tibs.__getitem__(index) + return self.tibs[index] def getslice_withstep_msb0(self, key: slice, /) -> ConstBitStore: - return ConstBitStore(self.tibs.__getitem__(key)) + return ConstBitStore(self.tibs[key]) def getslice_withstep_lsb0(self, key: slice, /) -> ConstBitStore: key = offset_slice_indices_lsb0(key, len(self)) - return ConstBitStore(self.tibs.__getitem__(key)) + return ConstBitStore(self.tibs[key]) def getslice_msb0(self, start: Optional[int], stop: Optional[int], /) -> ConstBitStore: return ConstBitStore(self.tibs[start:stop]) @@ -170,7 +168,7 @@ def getslice_lsb0(self, start: Optional[int], stop: Optional[int], /) -> ConstBi return ConstBitStore(self.tibs[s.start:s.stop]) def getindex_lsb0(self, index: int, /) -> bool: - return self.tibs.__getitem__(-index - 1) + return self.tibs[-index - 1] def any(self) -> bool: return self.tibs.any() @@ -338,14 +336,14 @@ def __getitem__(self, item: Union[int, slice], /) -> Union[int, MutableBitStore] raise NotImplementedError def getindex_msb0(self, index: int, /) -> bool: - return self.tibs.__getitem__(index) + return self.tibs[index] def getslice_withstep_msb0(self, key: slice, /) -> MutableBitStore: - return MutableBitStore(self.tibs.__getitem__(key)) + return MutableBitStore(self.tibs[key]) def getslice_withstep_lsb0(self, key: slice, /) -> MutableBitStore: key = offset_slice_indices_lsb0(key, len(self)) - return MutableBitStore(self.tibs.__getitem__(key)) + return MutableBitStore(self.tibs[key]) def getslice_msb0(self, start: Optional[int], stop: Optional[int], /) -> MutableBitStore: return MutableBitStore(self.tibs[start:stop]) @@ -355,7 +353,7 @@ def getslice_lsb0(self, start: Optional[int], stop: Optional[int], /) -> Mutable return MutableBitStore(self.tibs[s.start:s.stop]) def getindex_lsb0(self, index: int, /) -> bool: - return self.tibs.__getitem__(-index - 1) + return self.tibs[-index - 1] @overload def setitem_lsb0(self, key: int, value: int, /) -> None: From 409a797ec88158f1eb874306fabf667d6ae1f2e1 Mon Sep 17 00:00:00 2001 From: Scott Griffiths Date: Mon, 4 May 2026 17:00:30 +0100 Subject: [PATCH 16/35] Updating to use tibs 0.7. --- bitstring/bits.py | 2 +- bitstring/bitstore_tibs.py | 7 +++---- pyproject.toml | 2 +- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/bitstring/bits.py b/bitstring/bits.py index 6dfda9bd..03dcb44c 100644 --- a/bitstring/bits.py +++ b/bitstring/bits.py @@ -114,7 +114,7 @@ def __init__(self, auto: Optional[Union[BitsType, int]] = None, /, length: Optio pass def __new__(cls: Type[TBits], auto: Optional[Union[BitsType, int]] = None, /, length: Optional[int] = None, - offset: Optional[int] = None, pos: Optional[int] = None, **kwargs) -> TBits: + offset: Optional[int] = None, **kwargs) -> TBits: x = super().__new__(cls) if auto is None and not kwargs: # No initialiser so fill with zero bits up to length diff --git a/bitstring/bitstore_tibs.py b/bitstring/bitstore_tibs.py index d9562c0d..9d317e12 100644 --- a/bitstring/bitstore_tibs.py +++ b/bitstring/bitstore_tibs.py @@ -39,7 +39,7 @@ def __init__(self, initializer: Tibs) -> None: @classmethod def join(cls, bitstores: Iterable[ConstBitStore], /) -> ConstBitStore: x = super().__new__(cls) - x.tibs = Tibs.from_joined(b.tibs for b in bitstores) + x.tibs = Tibs.from_joined((b.tibs for b in bitstores)) return x @classmethod @@ -61,9 +61,8 @@ def from_bools(cls, iterable: Iterable[Any], /) -> ConstBitStore: return x @classmethod - def frombuffer(cls, buffer, /, length: Optional[int] = None) -> ConstBitStore: + def frombuffer(cls, buffer, /, length: Optional[int] = None) -> ConstBitStore: #TODO: Shouldn't need a default here. x = super().__new__(cls) - # from_bytes accepts memoryview, so avoid an eager bytes copy. x.tibs = Tibs.from_bytes(memoryview(buffer)) if length is not None: if length < 0: @@ -200,7 +199,7 @@ def __init__(self, initializer: Mutibs) -> None: @classmethod def join(cls, bitstores: Iterable[MutableBitStore], /) -> MutableBitStore: x = super().__new__(cls) - x.tibs = Mutibs.from_joined(b.tibs for b in bitstores) + x.tibs = Mutibs.from_joined((b.tibs for b in bitstores)) return x @classmethod diff --git a/pyproject.toml b/pyproject.toml index 81fb9a4e..bae52f95 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,7 +29,7 @@ classifiers = [ ] keywords = ["binary", "bitarray", "bitvector", "bitfield"] dependencies = [ - "tibs >= 0.6.0, < 0.7", + "tibs >= 0.7.0, < 0.8", ] [project.urls] From 1dc5936406297de21ca0cf57df56b030fa5d6f8b Mon Sep 17 00:00:00 2001 From: Scott Griffiths Date: Tue, 5 May 2026 07:39:30 +0100 Subject: [PATCH 17/35] Removing lsb0 mode. --- bitstring/__init__.py | 15 +- bitstring/array_.py | 2 +- bitstring/bitarray_.py | 13 +- bitstring/bits.py | 108 ++----------- bitstring/bitstore_tibs.py | 81 ++-------- bitstring/bitstring_options.py | 41 ----- bitstring/helpers.py | 18 --- bitstring/methods.py | 2 - doc/functions.rst | 96 ------------ doc/quick_reference.rst | 1 - release_notes.md | 6 + tests/test_bitarray.py | 277 --------------------------------- tests/test_bits.py | 165 -------------------- tests/test_bitstore.py | 67 -------- tests/test_bitstream.py | 116 -------------- tests/test_bitstring.py | 11 +- tests/test_constbitstream.py | 33 ---- 17 files changed, 47 insertions(+), 1005 deletions(-) diff --git a/bitstring/__init__.py b/bitstring/__init__.py index 37f829d6..3a7d2da3 100644 --- a/bitstring/__init__.py +++ b/bitstring/__init__.py @@ -83,8 +83,9 @@ options = Options() # These get defined properly by the module magic below. This just stops mypy complaining about them. -bytealigned = lsb0 = None +bytealigned = None +# TODO: REMOVE FOR VERSION 5 # An opaque way of adding module level properties. Taken from https://peps.python.org/pep-0549/ # This is now deprecated. Use the options object directly instead. @@ -99,16 +100,6 @@ def bytealigned(self, value: bool) -> None: """Determines whether a number of methods default to working only on byte boundaries.""" options.bytealigned = value - @property - def lsb0(self) -> bool: - """If True, the least significant bit (the final bit) is indexed as bit zero.""" - return options.lsb0 - - @lsb0.setter - def lsb0(self, value: bool) -> None: - """If True, the least significant bit (the final bit) is indexed as bit zero.""" - options.lsb0 = value - sys.modules[__name__].__class__ = _MyModuleType @@ -335,4 +326,4 @@ def bool_bits2chars(_: Literal[1]): __all__ = ['ConstBitStream', 'BitStream', 'BitArray', 'Array', 'Bits', 'pack', 'Error', 'ReadError', 'InterpretError', - 'ByteAlignError', 'CreationError', 'bytealigned', 'lsb0', 'Dtype', 'options'] + 'ByteAlignError', 'CreationError', 'bytealigned', 'Dtype', 'options'] diff --git a/bitstring/array_.py b/bitstring/array_.py index fe75bc51..8946e27c 100644 --- a/bitstring/array_.py +++ b/bitstring/array_.py @@ -439,7 +439,7 @@ def pp(self, fmt: Optional[str] = None, width: int = 120, length = len(self.data) // token_length len_str = colour.green + str(length) + colour.off stream.write(f"<{self.__class__.__name__} {tidy_fmt}, length={len_str}, itemsize={token_length} bits, total data size={(len(self.data) + 7) // 8} bytes> [\n") - data._pp(dtype1, dtype2, token_length, width, sep, format_sep, show_offset, stream, False, token_length) + data._pp(dtype1, dtype2, token_length, width, sep, format_sep, show_offset, stream, token_length) stream.write("]") if trailing_bit_length != 0: stream.write(" + trailing_bits = " + str(self.data[-trailing_bit_length:])) diff --git a/bitstring/bitarray_.py b/bitstring/bitarray_.py index 522c136d..3b094a10 100644 --- a/bitstring/bitarray_.py +++ b/bitstring/bitarray_.py @@ -312,9 +312,6 @@ def _replace(self, old: Bits, new: Bits, start: int, end: int, count: int, bytea # Final replacement replacement_list.append(new._bitstore) replacement_list.append(self._bitstore.getslice(starting_points[-1] + len(old), None)) - if bitstring.options.lsb0: - # Addition of bitarray is always on the right, so assemble from other end - replacement_list.reverse() self._bitstore.clear() for r in replacement_list: self._bitstore += r @@ -407,10 +404,10 @@ def prepend(self, bs: BitsType) -> None: """ self._prepend(bs) - def _append_msb0(self, bs: BitsType) -> None: + def _append(self, bs: BitsType) -> None: self._addright(self._create_from_bitstype(bs)) - def _append_lsb0(self, bs: BitsType) -> None: + def _prepend(self, bs: BitsType) -> None: bs = self._create_from_bitstype(bs) self._addleft(bs) @@ -497,8 +494,8 @@ def ror(self, bits: int, start: Optional[int] = None, end: Optional[int] = None) raise ValueError("Cannot rotate by negative amount.") self._ror(bits, start, end) - def _ror_msb0(self, bits: int, start: Optional[int] = None, end: Optional[int] = None) -> None: - start, end = self._validate_slice(start, end) # the _slice deals with msb0/lsb0 + def _ror(self, bits: int, start: Optional[int] = None, end: Optional[int] = None) -> None: + start, end = self._validate_slice(start, end) bits %= (end - start) if not bits: return @@ -522,7 +519,7 @@ def rol(self, bits: int, start: Optional[int] = None, end: Optional[int] = None) raise ValueError("Cannot rotate by negative amount.") self._rol(bits, start, end) - def _rol_msb0(self, bits: int, start: Optional[int] = None, end: Optional[int] = None): + def _rol(self, bits: int, start: Optional[int] = None, end: Optional[int] = None): start, end = self._validate_slice(start, end) bits %= (end - start) if bits == 0: diff --git a/bitstring/bits.py b/bitstring/bits.py index 03dcb44c..9c04f3ad 100644 --- a/bitstring/bits.py +++ b/bitstring/bits.py @@ -612,7 +612,7 @@ def _setbytes_with_truncation(self, data: Union[bytearray, bytes], length: Optio else: if length + offset > len(data) * 8: raise bitstring.CreationError(f"Not enough data present. Need {length + offset} bits, have {len(data) * 8}.") - self._bitstore = ConstBitStore.from_bytes(data).getslice_msb0(offset, offset + length) + self._bitstore = ConstBitStore.from_bytes(data).getslice(offset, offset + length) def _getbytes(self) -> bytes: """Return the data as an ordinary bytes object.""" @@ -801,8 +801,6 @@ def _setue(self, i: int) -> None: Raises CreationError if i < 0. """ - if bitstring.options.lsb0: - raise bitstring.CreationError("Exp-Golomb codes cannot be used in lsb0 mode.") self._bitstore = helpers.ue2bitstore(i) def _readue(self, pos: int) -> Tuple[int, int]: @@ -812,8 +810,6 @@ def _readue(self, pos: int) -> Tuple[int, int]: reading the code. """ - if bitstring.options.lsb0: - raise bitstring.ReadError("Exp-Golomb codes cannot be read in lsb0 mode.") oldpos = pos try: while not self[pos]: @@ -858,8 +854,6 @@ def _getsie(self) -> Tuple[int, int]: def _setse(self, i: int) -> None: """Initialise bitstring with signed exponential-Golomb code for integer i.""" - if bitstring.options.lsb0: - raise bitstring.CreationError("Exp-Golomb codes cannot be used in lsb0 mode.") self._bitstore = helpers.se2bitstore(i) def _readse(self, pos: int) -> Tuple[int, int]: @@ -881,8 +875,6 @@ def _setuie(self, i: int) -> None: Raises CreationError if i < 0. """ - if bitstring.options.lsb0: - raise bitstring.CreationError("Exp-Golomb codes cannot be used in lsb0 mode.") self._bitstore = helpers.uie2bitstore(i) def _readuie(self, pos: int) -> Tuple[int, int]: @@ -892,8 +884,6 @@ def _readuie(self, pos: int) -> Tuple[int, int]: reading the code. """ - if bitstring.options.lsb0: - raise bitstring.ReadError("Exp-Golomb codes cannot be read in lsb0 mode.") try: codenum: int = 1 while not self[pos]: @@ -908,8 +898,6 @@ def _readuie(self, pos: int) -> Tuple[int, int]: def _setsie(self, i: int, ) -> None: """Initialise bitstring with signed interleaved exponential-Golomb code for integer i.""" - if bitstring.options.lsb0: - raise bitstring.CreationError("Exp-Golomb codes cannot be used in lsb0 mode.") self._bitstore = helpers.sie2bitstore(i) def _readsie(self, pos: int) -> Tuple[int, int]: @@ -994,13 +982,12 @@ def _slice(self: TBits, start: int, end: int) -> TBits: return bs def _absolute_slice(self: TBits, start: int, end: int) -> TBits: - """Used internally to get a slice, without error checking. - Uses MSB0 bit numbering even if LSB0 is set.""" + """Used internally to get a slice, without error checking.""" if end == start: return self.__class__() assert start < end, f"start={start}, end={end}" bs = self.__class__() - bs._bitstore = self._bitstore.getslice_msb0(start, end) + bs._bitstore = self._bitstore.getslice(start, end) return bs def _readtoken(self, name: str, pos: int, length: Optional[int]) -> Tuple[Union[float, int, str, None, Bits], int]: @@ -1184,19 +1171,7 @@ def find(self, bs: BitsType, /, start: Optional[int] = None, end: Optional[int] p = self._find(bs, start, end, ba) return p - def _find_lsb0(self, bs: Bits, start: int, end: int, bytealigned: bool) -> Union[Tuple[int], Tuple[()]]: - # A forward find in lsb0 is very like a reverse find in msb0. - assert bitstring.options.lsb0 - - new_slice = bitstring.helpers.offset_slice_indices_lsb0(slice(start, end, None), len(self)) - p = self._rfind_msb0(bs, new_slice.start, new_slice.stop, bytealigned) - - if p: - return (len(self) - p[0] - len(bs),) - else: - return () - - def _find_msb0(self, bs: Bits, start: int, end: int, bytealigned: bool) -> Union[Tuple[int], Tuple[()]]: + def _find(self, bs: Bits, start: int, end: int, bytealigned: bool) -> Union[Tuple[int], Tuple[()]]: """Find first occurrence of a binary string.""" p = self._bitstore.find(bs._bitstore, start, end, bytealigned) return () if p is None else (p,) @@ -1225,46 +1200,16 @@ def findall(self, bs: BitsType, start: Optional[int] = None, end: Optional[int] ba = bitstring.options.bytealigned if bytealigned is None else bytealigned return self._findall(bs, start, end, count, ba) - def _findall_msb0(self, bs: Bits, start: int, end: int, count: Optional[int], + def _findall(self, bs: Bits, start: int, end: int, count: Optional[int], bytealigned: bool) -> Iterable[int]: c = 0 - for i in self._bitstore.findall_msb0(bs._bitstore, start, end, bytealigned): + for i in self._bitstore.findall(bs._bitstore, start, end, bytealigned): if count is not None and c >= count: return c += 1 yield i return - def _findall_lsb0(self, bs: Bits, start: int, end: int, count: Optional[int], - bytealigned: bool) -> Iterable[int]: - assert bitstring.options.lsb0 - - new_slice = bitstring.helpers.offset_slice_indices_lsb0(slice(start, end, None), len(self)) - msb0_start, msb0_end = new_slice.start, new_slice.stop - - # Search chunks starting near the end and then moving back. - c = 0 - increment = max(8192, len(bs) * 80) - buffersize = min(increment + len(bs), msb0_end - msb0_start) - pos = max(msb0_start, msb0_end - buffersize) - while True: - found = list(self._findall_msb0(bs, start=pos, end=pos + buffersize, count=None, bytealigned=False)) - if not found: - if pos == msb0_start: - return - pos = max(msb0_start, pos - increment) - continue - while found: - if count is not None and c >= count: - return - c += 1 - lsb0_pos = len(self) - found.pop() - len(bs) - if not bytealigned or lsb0_pos % 8 == 0: - yield lsb0_pos - - pos = max(msb0_start, pos - increment) - if pos == msb0_start: - return def rfind(self, bs: BitsType, /, start: Optional[int] = None, end: Optional[int] = None, bytealigned: Optional[bool] = None) -> Union[Tuple[int], Tuple[()]]: @@ -1290,23 +1235,11 @@ def rfind(self, bs: BitsType, /, start: Optional[int] = None, end: Optional[int] p = self._rfind(bs, start, end, ba) return p - def _rfind_msb0(self, bs: Bits, start: int, end: int, bytealigned: bool) -> Union[Tuple[int], Tuple[()]]: + def _rfind(self, bs: Bits, start: int, end: int, bytealigned: bool) -> Union[Tuple[int], Tuple[()]]: """Find final occurrence of a binary string.""" p = self._bitstore.rfind(bs._bitstore, start, end, bytealigned) return () if p is None else (p,) - def _rfind_lsb0(self, bs: Bits, start: int, end: int, bytealigned: bool) -> Union[Tuple[int], Tuple[()]]: - # A reverse find in lsb0 is very like a forward find in msb0. - assert bitstring.options.lsb0 - new_slice = bitstring.helpers.offset_slice_indices_lsb0(slice(start, end, None), len(self)) - msb0_start, msb0_end = new_slice.start, new_slice.stop - - p = self._find_msb0(bs, msb0_start, msb0_end, bytealigned) - if p: - return (len(self) - p[0] - len(bs),) - else: - return () - def cut(self, bits: int, start: Optional[int] = None, end: Optional[int] = None, count: Optional[int] = None) -> Iterator[Bits]: """Return bitstring generator by cutting into bits sized chunks. @@ -1363,7 +1296,7 @@ def split(self, delimiter: BitsType, start: Optional[int] = None, end: Optional[ raise ValueError("Cannot split - count must be >= 0.") if count == 0: return - f = functools.partial(self._find_msb0, bs=delimiter, bytealigned=bytealigned_) + f = functools.partial(self._find, bs=delimiter, bytealigned=bytealigned_) found = f(start=start, end=end) if not found: # Initial bits are the whole bitstring being searched @@ -1511,8 +1444,8 @@ def _format_bits(bits: Bits, bits_per_group: int, sep: str, dtype: Dtype, if bits_per_group == 0: x = str(get_fn(bits)) else: - # Left-align for fixed width types when msb0, otherwise right-align. - align = '<' if dtype.name in ['bin', 'oct', 'hex', 'bits', 'bytes'] and not bitstring.options.lsb0 else '>' + # Left-align for fixed width types + align = '<' if dtype.name in ['bin', 'oct', 'hex', 'bits', 'bytes'] else '>' chars_per_group = 0 if dtype_register[dtype.name].bitlength2chars_fn is not None: chars_per_group = dtype_register[dtype.name].bitlength2chars_fn(bits_per_group) @@ -1522,10 +1455,7 @@ def _format_bits(bits: Bits, bits_per_group: int, sep: str, dtype: Dtype, padding_spaces = 0 if width is None else max(width - len(x), 0) x = colour_start + x + colour_end # Pad final line with spaces to align it - if bitstring.options.lsb0: - x = ' ' * padding_spaces + x - else: - x += ' ' * padding_spaces + x += ' ' * padding_spaces return x, chars_used @staticmethod @@ -1543,7 +1473,7 @@ def _bits_per_char(fmt: str): return 24 // dtype_register[fmt].bitlength2chars_fn(24) def _pp(self, dtype1: Dtype, dtype2: Optional[Dtype], bits_per_group: int, width: int, sep: str, format_sep: str, - show_offset: bool, stream: TextIO, lsb0: bool, offset_factor: int) -> None: + show_offset: bool, stream: TextIO, offset_factor: int) -> None: """Internal pretty print method.""" colour = Colour(not bitstring.options.no_color) name1 = dtype1.name @@ -1553,7 +1483,7 @@ def _pp(self, dtype1: Dtype, dtype2: Optional[Dtype], bits_per_group: int, width if dtype2 is not None and dtype2.variable_length: raise ValueError(f"Can't use Dtype '{dtype2}' in pp() as it has a variable length.") offset_width = 0 - offset_sep = ' :' if lsb0 else ': ' + offset_sep = ': ' if show_offset: # This could be 1 too large in some circumstances. Slightly recurrent logic needed to fix it... offset_width = len(str(len(self))) + len(offset_sep) @@ -1587,10 +1517,7 @@ def _pp(self, dtype1: Dtype, dtype2: Optional[Dtype], bits_per_group: int, width if show_offset: offset = bitpos // offset_factor bitpos += len(bits) - if bitstring.options.lsb0: - offset_str = colour.green + offset_sep + f'{offset: <{offset_width - len(offset_sep)}}' + colour.off - else: - offset_str = colour.green + f'{offset: >{offset_width - len(offset_sep)}}' + offset_sep + colour.off + offset_str = colour.green + f'{offset: >{offset_width - len(offset_sep)}}' + offset_sep + colour.off fb1, chars_used = Bits._format_bits(bits, bits_per_group, sep, dtype1, colour.purple, colour.off, first_fb_width) if first_fb_width is None: @@ -1603,10 +1530,7 @@ def _pp(self, dtype1: Dtype, dtype2: Optional[Dtype], bits_per_group: int, width second_fb_width = chars_used fb2 = format_sep + fb2 - if bitstring.options.lsb0 is True: - line_fmt = fb1 + fb2 + offset_str + '\n' - else: - line_fmt = offset_str + fb1 + fb2 + '\n' + line_fmt = offset_str + fb1 + fb2 + '\n' stream.write(line_fmt) return @@ -1678,7 +1602,7 @@ def pp(self, fmt: Optional[str] = None, width: int = 120, sep: str = ' ', len_str = colour.green + str(len(self)) + colour.off output_stream.write(f"<{self.__class__.__name__}, fmt='{tidy_fmt}', length={len_str} bits> [\n") data._pp(dtype1, dtype2, bits_per_group, width, sep, format_sep, show_offset, - output_stream, bitstring.options.lsb0, 1) + output_stream, 1) output_stream.write("]") if trailing_bit_length != 0: output_stream.write(" + trailing_bits = " + str(self[-trailing_bit_length:])) diff --git a/bitstring/bitstore_tibs.py b/bitstring/bitstore_tibs.py index 9d317e12..7cf83236 100644 --- a/bitstring/bitstore_tibs.py +++ b/bitstring/bitstore_tibs.py @@ -4,7 +4,6 @@ from bitstring.exceptions import CreationError from typing import Union, Iterable, Optional, overload, Iterator, Any -from bitstring.helpers import offset_slice_indices_lsb0 def _fallback_to_u(tibs: Tibs | Mutibs) -> int: @@ -29,7 +28,7 @@ def _fallback_to_i(tibs: Tibs | Mutibs) -> int: class ConstBitStore: - """A light wrapper around tibs.Tibs that does the LSB0 stuff""" + """A light wrapper around tibs.Tibs""" __slots__ = ('tibs',) @@ -129,10 +128,10 @@ def find(self, bs: ConstBitStore, start: int, end: int, bytealigned: bool = Fals def rfind(self, bs: ConstBitStore, start: int, end: int, bytealigned: bool = False) -> int | None: return self.tibs.rfind(bs.tibs, start, end, byte_aligned=bytealigned) - def findall_msb0(self, bs: ConstBitStore, start: int, end: int, bytealigned: bool = False) -> Iterator[int]: + def findall(self, bs: ConstBitStore, start: int, end: int, bytealigned: bool = False) -> Iterator[int]: return self.tibs.find_all(bs.tibs, start=start, end=end, byte_aligned=bytealigned) - def rfindall_msb0(self, bs: ConstBitStore, start: int, end: int, bytealigned: bool = False) -> Iterator[int]: + def rfindall(self, bs: ConstBitStore, start: int, end: int, bytealigned: bool = False) -> Iterator[int]: return self.tibs.rfind_all(bs.tibs, start=start, end=end, byte_aligned=bytealigned) def __iter__(self) -> Iterable[bool]: @@ -149,26 +148,15 @@ def __getitem__(self, item: Union[int, slice], /) -> Union[int, ConstBitStore]: # Use getindex or getslice instead raise NotImplementedError - def getindex_msb0(self, index: int, /) -> bool: + def getindex(self, index: int, /) -> bool: return self.tibs[index] - def getslice_withstep_msb0(self, key: slice, /) -> ConstBitStore: + def getslice_withstep(self, key: slice, /) -> ConstBitStore: return ConstBitStore(self.tibs[key]) - def getslice_withstep_lsb0(self, key: slice, /) -> ConstBitStore: - key = offset_slice_indices_lsb0(key, len(self)) - return ConstBitStore(self.tibs[key]) - - def getslice_msb0(self, start: Optional[int], stop: Optional[int], /) -> ConstBitStore: + def getslice(self, start: Optional[int], stop: Optional[int], /) -> ConstBitStore: return ConstBitStore(self.tibs[start:stop]) - def getslice_lsb0(self, start: Optional[int], stop: Optional[int], /) -> ConstBitStore: - s = offset_slice_indices_lsb0(slice(start, stop, None), len(self)) - return ConstBitStore(self.tibs[s.start:s.stop]) - - def getindex_lsb0(self, index: int, /) -> bool: - return self.tibs[-index - 1] - def any(self) -> bool: return self.tibs.any() @@ -189,7 +177,7 @@ def __len__(self) -> int: class MutableBitStore: - """A light wrapper around tibs.Mutibs that does the LSB0 stuff""" + """A light wrapper around tibs.Mutibs""" __slots__ = ('tibs',) @@ -304,10 +292,10 @@ def find(self, bs: MutableBitStore, start: int, end: int, bytealigned: bool = Fa def rfind(self, bs: MutableBitStore, start: int, end: int, bytealigned: bool = False): return self.tibs.rfind(bs.tibs, start, end, byte_aligned=bytealigned) - def findall_msb0(self, bs: MutableBitStore, start: int, end: int, bytealigned: bool = False) -> Iterator[int]: + def findall(self, bs: MutableBitStore, start: int, end: int, bytealigned: bool = False) -> Iterator[int]: return self.tibs.to_tibs().find_all(bs.tibs, start=start, end=end, byte_aligned=bytealigned) - def rfindall_msb0(self, bs: MutableBitStore, start: int, end: int, bytealigned: bool = False) -> Iterator[int]: + def rfindall(self, bs: MutableBitStore, start: int, end: int, bytealigned: bool = False) -> Iterator[int]: return self.tibs.to_tibs().rfind_all(bs.tibs, start=start, end=end, byte_aligned=bytealigned) def clear(self) -> None: @@ -334,60 +322,21 @@ def __getitem__(self, item: Union[int, slice], /) -> Union[int, MutableBitStore] # Use getindex or getslice instead raise NotImplementedError - def getindex_msb0(self, index: int, /) -> bool: + def getindex(self, index: int, /) -> bool: return self.tibs[index] - def getslice_withstep_msb0(self, key: slice, /) -> MutableBitStore: + def getslice_withstep(self, key: slice, /) -> MutableBitStore: return MutableBitStore(self.tibs[key]) - def getslice_withstep_lsb0(self, key: slice, /) -> MutableBitStore: - key = offset_slice_indices_lsb0(key, len(self)) - return MutableBitStore(self.tibs[key]) - - def getslice_msb0(self, start: Optional[int], stop: Optional[int], /) -> MutableBitStore: + def getslice(self, start: Optional[int], stop: Optional[int], /) -> MutableBitStore: return MutableBitStore(self.tibs[start:stop]) - def getslice_lsb0(self, start: Optional[int], stop: Optional[int], /) -> MutableBitStore: - s = offset_slice_indices_lsb0(slice(start, stop, None), len(self)) - return MutableBitStore(self.tibs[s.start:s.stop]) - - def getindex_lsb0(self, index: int, /) -> bool: - return self.tibs[-index - 1] - - @overload - def setitem_lsb0(self, key: int, value: int, /) -> None: - ... - - @overload - def setitem_lsb0(self, key: slice, value: MutableBitStore, /) -> None: - ... - - def setitem_lsb0(self, key: Union[int, slice], value: Union[int, MutableBitStore], /) -> None: - if isinstance(key, slice): - new_slice = offset_slice_indices_lsb0(key, len(self)) - self.tibs.__setitem__(new_slice, value.tibs) - else: - self.tibs.__setitem__(-key - 1, bool(value)) - - def delitem_lsb0(self, key: Union[int, slice], /) -> None: - if isinstance(key, slice): - new_slice = offset_slice_indices_lsb0(key, len(self)) - self.tibs.__delitem__(new_slice) - else: - self.tibs.__delitem__(-key - 1) - - def invert_msb0(self, index: Optional[int] = None, /) -> None: + def invert(self, index: Optional[int] = None, /) -> None: if index is not None: self.tibs.invert(index) else: self.tibs.invert() - def invert_lsb0(self, index: Optional[int] = None, /) -> None: - if index is not None: - self.tibs.invert(-index - 1) - else: - self.tibs.invert() - def any(self) -> bool: return self.tibs.any() @@ -417,7 +366,7 @@ def rotate_right(self, n: int, start: Optional[int] = None, end: Optional[int] = def __len__(self) -> int: return len(self.tibs) - def setitem_msb0(self, key, value, /): + def __setitem__(self, key, value, /): if isinstance(value, (MutableBitStore, ConstBitStore)): self.tibs.__setitem__(key, value.tibs) else: @@ -428,5 +377,5 @@ def setitem_msb0(self, key, value, /): else: self.tibs.unset(key) - def delitem_msb0(self, key, /): + def __delitem__(self, key, /): self.tibs.__delitem__(key) diff --git a/bitstring/bitstring_options.py b/bitstring/bitstring_options.py index e3c8272d..d3d1976c 100644 --- a/bitstring/bitstring_options.py +++ b/bitstring/bitstring_options.py @@ -12,7 +12,6 @@ class Options: _instance = None def __init__(self): - self.set_lsb0(False) self._bytealigned = False self.mxfp_overflow = 'saturate' @@ -35,46 +34,6 @@ def __repr__(self) -> str: attributes = {attr: getattr(self, attr) for attr in dir(self) if not attr.startswith('_') and not callable(getattr(self, attr))} return '\n'.join(f"{attr}: {value!r}" for attr, value in attributes.items()) - @property - def lsb0(self) -> bool: - return self._lsb0 - - @lsb0.setter - def lsb0(self, value: bool) -> None: - self.set_lsb0(value) - - def set_lsb0(self, value: bool) -> None: - self._lsb0 = bool(value) - Bits = bitstring.bits.Bits - BitArray = bitstring.bitarray_.BitArray - - lsb0_methods = { - Bits: {'_find': Bits._find_lsb0, '_rfind': Bits._rfind_lsb0, '_findall': Bits._findall_lsb0}, - BitArray: {'_ror': BitArray._rol_msb0, '_rol': BitArray._ror_msb0, '_append': BitArray._append_lsb0, - '_prepend': BitArray._append_msb0}, - ConstBitStore: {'__setitem__': None, '__delitem__': None, 'invert': None, - 'getindex': ConstBitStore.getindex_lsb0, 'getslice': ConstBitStore.getslice_lsb0, - 'getslice_withstep': ConstBitStore.getslice_withstep_lsb0}, - MutableBitStore: {'__setitem__': MutableBitStore.setitem_lsb0, '__delitem__': MutableBitStore.delitem_lsb0, - 'getindex': MutableBitStore.getindex_lsb0, 'getslice': MutableBitStore.getslice_lsb0, - 'getslice_withstep': MutableBitStore.getslice_withstep_lsb0, 'invert': MutableBitStore.invert_lsb0} - } - msb0_methods = { - Bits: {'_find': Bits._find_msb0, '_rfind': Bits._rfind_msb0, '_findall': Bits._findall_msb0}, - BitArray: {'_ror': BitArray._ror_msb0, '_rol': BitArray._rol_msb0, '_append': BitArray._append_msb0, - '_prepend': BitArray._append_lsb0}, - ConstBitStore: {'__setitem__': None, '__delitem__': None, 'invert': None, - 'getindex': ConstBitStore.getindex_msb0, 'getslice': ConstBitStore.getslice_msb0, - 'getslice_withstep': ConstBitStore.getslice_withstep_msb0}, - MutableBitStore: {'__setitem__': MutableBitStore.setitem_msb0, '__delitem__': MutableBitStore.delitem_msb0, - 'getindex': MutableBitStore.getindex_msb0, 'getslice': MutableBitStore.getslice_msb0, - 'getslice_withstep': MutableBitStore.getslice_withstep_msb0, 'invert': MutableBitStore.invert_msb0} - } - methods = lsb0_methods if self._lsb0 else msb0_methods - for cls, method_dict in methods.items(): - for attr, method in method_dict.items(): - setattr(cls, attr, method) - @property def bytealigned(self) -> bool: return self._bytealigned diff --git a/bitstring/helpers.py b/bitstring/helpers.py index 3cf3d4f5..33196a00 100644 --- a/bitstring/helpers.py +++ b/bitstring/helpers.py @@ -12,24 +12,6 @@ def _indices(s: slice, length: int) -> Tuple[int, Union[int, None], int]: stop = None return start, stop, step -def offset_slice_indices_lsb0(key: slice, length: int) -> slice: - start, stop, step = _indices(key, length) - if step is not None and step < 0: - if stop is None: - new_start = start + 1 - new_stop = None - else: - first_element = start - last_element = start + ((stop + 1 - start) // step) * step - new_start = length - last_element - new_stop = length - first_element - 1 - else: - first_element = start - # The last element will usually be stop - 1, but needs to be adjusted if step != 1. - last_element = start + ((stop - 1 - start) // step) * step - new_start = length - last_element - 1 - new_stop = length - first_element - return slice(new_start, new_stop, key.step) def tidy_input_string(s: str) -> str: """Return string made lowercase and with all whitespace and underscores removed.""" diff --git a/bitstring/methods.py b/bitstring/methods.py index 08f247b8..618ebf8c 100644 --- a/bitstring/methods.py +++ b/bitstring/methods.py @@ -80,8 +80,6 @@ def pack(fmt: Union[str, List[str]], *values, **kwargs) -> BitStream: except StopIteration: # Good, we've used up all the *values. s = BitStream() - if bitstring.options.lsb0: - bsl.reverse() for b in bsl: s._bitstore += b return s diff --git a/doc/functions.rst b/doc/functions.rst index 94b0a764..c56d7e6a 100644 --- a/doc/functions.rst +++ b/doc/functions.rst @@ -92,102 +92,6 @@ Options ------- The bitstring module has an ``options`` object that allows certain module-wide behaviours to be set. -lsb0 -^^^^ - -.. data:: bitstring.options.lsb0 : bool - -By default bit numbering in the bitstring module is done from 'left' to 'right'. That is, from bit ``0`` at the start of the data to bit ``n - 1`` at the end. This allows bitstrings to be treated like an ordinary Python container that is only allowed to contain single bits. - - -The ``lsb0`` option allows bitstrings to use Least Significant Bit Zero -(LSB0) bit numbering; that is the right-most bit in the bitstring will -be bit 0, and the left-most bit will be bit (n-1), rather than the -other way around. LSB0 is a more natural numbering -system in many fields, but is the opposite to Most Significant Bit -Zero (MSB0) numbering which is the natural option when thinking of -bitstrings as standard Python containers. - -For example, if you set a bitstring to be the binary ``010001111`` it will be stored in the same way for MSB0 and LSB0 but slicing, reading, unpacking etc. will all behave differently. - -.. list-table:: MSB0 → - :header-rows: 1 - - * - bit index - - 0 - - 1 - - 2 - - 3 - - 4 - - 5 - - 6 - - 7 - - 8 - * - value - - ``0`` - - ``1`` - - ``0`` - - ``0`` - - ``0`` - - ``1`` - - ``1`` - - ``1`` - - ``1`` - -In MSB0 everything behaves like an ordinary Python container. Bit zero is the left-most bit and reads/slices happen from left to right. - -.. list-table:: ← LSB0 - :header-rows: 1 - - * - bit index - - 8 - - 7 - - 6 - - 5 - - 4 - - 3 - - 2 - - 1 - - 0 - * - value - - ``0`` - - ``1`` - - ``0`` - - ``0`` - - ``0`` - - ``1`` - - ``1`` - - ``1`` - - ``1`` - -In LSB0 the final, right-most bit is labelled as bit zero. Reads and slices happen from right to left. - -When bitstrings (or slices of bitstrings) are interpreted as integers and other types the left-most bit is considered as the most significant bit. It's important to note that this is the case irrespective of whether the first or last bit is considered the bit zero, so for example if you were to interpret a whole bitstring as an integer, its value would be the same with and without `lsb0` being set to `True`. - -To illustrate this, for the example above this means that the bin and int representations would be ``010001111`` and ``143`` respectively for both MSB0 and LSB0 bit numbering. - -To switch from the default MSB0, use ``bitstring.options.lsb0``. This defaults to ``False`` and unless explicitly stated all examples and documentation related to the bitstring module use the default MSB0 indexing. - - >>> bitstring.options.lsb0 = True - -Slicing is still done with the start bit smaller than the end bit. -For example: - - >>> s = Bits('0b010001111') - >>> s[0:5] # LSB0 so this is the right-most five bits - Bits('0b01111') - >>> s[0] - True - -.. note:: - In some standards and documents using LSB0 notation the slice of the final five bits would be shown as ``s[5:0]``, which is reasonable as bit 5 comes before bit 0 when reading left to right, but this notation isn't used in this module as it clashes too much with the usual Python notation. - -Negative indices work as you'd expect, with the first stored -bit being ``s[-1]`` and the final stored bit being ``s[-n]``. - -Reading, peeking and unpacking of bitstrings are also affected by the ``lsb0`` flag, so reading always increments the bit position, and will move from right to left if ``lsb0`` is ``True``. Because of the way that exponential-Golomb codes are read (with the left-most bits determining the length of the code) these interpretations are not available in LSB0 mode, and using them will raise an exception. - -For ``BitStream`` and ``ConstBitStream`` objects changing the value of ``bitstring.options.lsb0`` invalidates the current position in the bitstring, unless that value is ``0``, and future results are undefined. Basically don't perform reads or change the current bit position before switching the bit numbering system! bytealigned ^^^^^^^^^^^ diff --git a/doc/quick_reference.rst b/doc/quick_reference.rst index d9cd1702..972a5035 100644 --- a/doc/quick_reference.rst +++ b/doc/quick_reference.rst @@ -533,6 +533,5 @@ Options The ``bitstring.options`` object contains module level options that can be changed to affect the behaviour of the module. * :data:`~bitstring.options.bytealigned` -- Determines whether a number of methods default to working only on byte boundaries. -* :data:`~bitstring.options.lsb0` -- If True, index bits with the least significant bit (the final bit) as bit zero. * :data:`~bitstring.options.mxfp_overflow` -- Determines how values are converted to 8-bit MX floats. Can be either ``'saturate'`` (the default) or ``'overflow'``. See :ref:`Exotic floats`. * :data:`~bitstring.options.no_color` -- If True, don't use ANSI color codes in the pretty print methods. Defaults to False unless the NO_COLOR environment variable is set. diff --git a/release_notes.md b/release_notes.md index 203f954e..07b5d583 100644 --- a/release_notes.md +++ b/release_notes.md @@ -1,6 +1,12 @@ # Release Notes +### Upcoming: version 5.0 + +Breaking changes: + +* Removed LSB0 mode. + ### March 2026: version 4.4.0 This version adds a new optional Rust-based backend. This is turned off by default so diff --git a/tests/test_bitarray.py b/tests/test_bitarray.py index 0954898e..b5288897 100644 --- a/tests/test_bitarray.py +++ b/tests/test_bitarray.py @@ -361,283 +361,6 @@ def test_adding(self): assert b == '0b11' -class TestLsb0Setting: - - @classmethod - def setup_class(cls): - bitstring.lsb0 = True - - @classmethod - def teardown_class(cls): - bitstring.lsb0 = False - - def test_set_single_bit(self): - a = BitArray(10) - a[0] = True - assert a == '0b0000000001' - a[1] = True - assert a == '0b0000000011' - a[0] = False - assert a == '0b0000000010' - a[9] = True - assert a == '0b1000000010' - with pytest.raises(IndexError): - a[10] = True - - def test_set_single_negative_bit(self): - a = BitArray('0o000') - a[-1] = True - assert a == '0b100000000' - a[-2] = True - assert a == '0o600' - a[-9] = True - assert a == '0o601' - with pytest.raises(IndexError): - a[-10] = True - - def test_invert_bit(self): - a = BitArray('0b11110000') - a.invert() - assert a == '0x0f' - a.invert(0) - assert a == '0b00001110' - a.invert(-1) - assert a == '0b10001110' - - def test_deleting_bits(self): - a = BitArray('0b11110') - del a[0] - assert a == '0xf' - - def test_deleting_range(self): - a = BitArray('0b101111000') - del a[0:1] - assert a == '0b10111100' - del a[2:6] - assert a == '0b1000' - a = BitArray('0xabcdef') - del a[:8] - assert a == '0xabcd' - del a[-4:] - assert a == '0xbcd' - del a[:-4] - assert a == '0xb' - - def test_appending_bits(self): - a = BitArray('0b111') - a.append('0b000') - assert a.bin == '000111' - a += '0xabc' - assert a == '0xabc, 0b000111' - - def test_setting_slice(self): - a = BitArray('0x012345678') - a[4:12] = '0xfe' - assert a == '0x012345fe8' - a[0:4] = '0xbeef' - assert a == '0x012345febeef' - - def test_truncating_start(self): - a = BitArray('0b1110000') - a = a[4:] - assert a == '0b111' - - def test_truncating_end(self): - a = BitArray('0x123456') - a = a[:16] - assert a == '0x3456' - - def test_all(self): - a = BitArray('0b0000101') - assert a.all(1, [0, 2]) - assert a.all(False, [-1, -2, -3, -4]) - - b = Bits(bytes=b'\x00\xff\xff', offset=7) - assert b.all(1, [1, 2, 3, 4, 5, 6, 7]) - assert b.all(1, [-2, -3, -4, -5, -6, -7, -8]) - - def test_any(self): - a = BitArray('0b0001') - assert a.any(1, [0, 1, 2]) - - def test_endswith(self): - a = BitArray('0xdeadbeef') - assert a.endswith('0xdead') - - def test_startswith(self): - a = BitArray('0xdeadbeef') - assert a.startswith('0xbeef') - - def test_cut(self): - a = BitArray('0xff00ff1111ff2222') - li = list(a.cut(16)) - assert li == ['0x2222', '0x11ff', '0xff11', '0xff00'] - - def test_find(self): - t = BitArray('0b10') - p, = t.find('0b1') - assert p == 1 - t = BitArray('0b1010') - p, = t.find('0b1') - assert p == 1 - a = BitArray('0b10101010, 0xabcd, 0b10101010, 0x0') - p, = a.find('0b10101010', bytealigned=False) - assert p == 4 - p, = a.find('0b10101010', start=4, bytealigned=False) - assert p == 4 - p, = a.find('0b10101010', start=5, bytealigned=False) - assert p == 22 - - def test_find_failing(self): - a = BitArray() - p = a.find('0b1') - assert p == () - a = BitArray('0b11111111111011') - p = a.find('0b100') - assert not p - - def test_find_failing2(self): - s = BitArray('0b101') - p, = s.find('0b1', start=2) - assert p == 2 - - def test_rfind(self): - a = BitArray('0b1000000') - p = a.rfind('0b1') - assert p == (6,) - p = a.rfind('0b000') - assert p == (3,) - - def test_rfind_with_start_and_end(self): - a = BitArray('0b11 0000 11 00') - p = a.rfind('0b11', start=8) - assert p[0] == 8 - p = a.rfind('0b110', start=8) - assert p == () - p = a.rfind('0b11', end=-1) - assert p[0] == 2 - - def test_findall(self): - a = BitArray('0b001000100001') - b = list(a.findall('0b1')) - assert b == [0, 5, 9] - c = list(a.findall('0b0001')) - assert c == [0, 5] - d = list(a.findall('0b10')) - assert d == [4, 8] - e = list(a.findall('0x198273641234')) - assert e == [] - - def test_find_all_with_start_and_end(self): - a = BitArray('0xaabbccaabbccccbb') - b = list(a.findall('0xbb', start=0, end=8)) - assert b == [0] - b = list(a.findall('0xbb', start=1, end=8)) - assert b == [] - b = list(a.findall('0xbb', start=0, end=7)) - assert b == [] - b = list(a.findall('0xbb', start=48)) - assert b == [48] - b = list(a.findall('0xbb', start=47)) - assert b == [48] - b = list(a.findall('0xbb', start=49)) - assert b == [] - - def test_find_all_byte_aligned(self): - a = BitArray('0x0550550') - b = list(a.findall('0x55', bytealigned=True)) - assert b == [16] - - def test_find_all_with_count(self): - a = BitArray('0b0001111101') - b = list(a.findall([1], start=1, count=1)) - assert b == [2] - - def test_split(self): - a = BitArray('0x4700004711472222') - li = list(a.split('0x47', bytealigned=True)) - assert li == ['', '0x472222', '0x4711', '0x470000'] - - def test_byte_swap(self): - a = BitArray('0xaa00ff00ff00') - n = a.byteswap(2, end=32, repeat=True) - assert n == 2 - assert a == '0xaa0000ff00ff' - - def test_insert(self): - a = BitArray('0x0123456') - a.insert('0xf', 4) - assert a == '0x012345f6' - - def test_overwrite(self): - a = BitArray('0x00000000') - a.overwrite('0xdead', 4) - assert a == '0x000dead0' - - def test_replace(self): - a = BitArray('0x5551100') - n = a.replace('0x1', '0xabc') - assert n == 2 - assert a == '0x555abcabc00' - n = a.replace([1], [0], end=12) - assert n == 2 - assert a == '0x555abcab000' - - def test_reverse(self): - a = BitArray('0x0011223344') - a.reverse() - assert a == '0x22cc448800' - a.reverse(0, 16) - assert a == '0x22cc440011' - - def test_ror(self): - a = BitArray('0b111000') - a.ror(1) - assert a == '0b011100' - a = BitArray('0b111000') - a.ror(1, start=2, end=6) - assert a == '0b011100' - - def test_rol(self): - a = BitArray('0b1') - a.rol(12) - assert a == '0b1' - b = BitArray('0b000010') - b.rol(3) - assert b == '0b010000' - - def test_set(self): - a = BitArray(100) - a.set(1, [0, 2, 4]) - assert a[0] - assert a.startswith('0b000010101') - a = BitArray('0b111') - a.set(False, 0) - assert a == '0b110' - - def test_failing_repr(self): - a = BitArray('0b010') - a.find('0b1') - assert repr(a) == "BitArray('0b010')" - - def test_left_shift(self): - a = BitArray('0b11001') - assert (a << 1).b == '10010' - assert (a << 5).b == '00000' - assert (a << 0).b == '11001' - - def test_right_shift(self): - a = BitArray('0b11001') - assert (a >> 1).b == '01100' - assert (a >> 5).b == '00000' - assert (a >> 0).b == '11001' - - # def testConstFileBased(self): - # filename = os.path.join(THIS_DIR, 'test.m1v') - # a = Bits(filename=filename, offset=8) - # self.assertTrue(a[-8]) - # self.assertTrue(a.endswith('0x01b3')) - class TestRepr: diff --git a/tests/test_bits.py b/tests/test_bits.py index b030acd9..faba9665 100644 --- a/tests/test_bits.py +++ b/tests/test_bits.py @@ -9,7 +9,6 @@ from bitstring import InterpretError, Bits, BitArray from hypothesis import given, settings import hypothesis.strategies as st -from bitstring.helpers import offset_slice_indices_lsb0 sys.path.insert(0, "..") @@ -21,33 +20,6 @@ def remove_unprintable(s: str) -> str: return colour_escape.sub("", s) -@settings(max_examples=500) -@given( - length=st.integers(0, 9), - start=st.integers(-20, 20), - stop=st.integers(-20, 20), - step=st.integers(0, 7), -) -def test_lsb0_slicing(length, start, stop, step): - if start == -20: - start = None - if stop == -20: - stop = None - if step == 0: - step = None - values_fwd = list(range(0, length)) - values_bwd = list(range(0, length)) - values_bwd.reverse() - - # Convert the start, stop, step to a range over the length - start1, stop1, step1 = slice(start, stop, step).indices(length) - values1 = values_fwd[start1:stop1:step1] - - lsb0key = offset_slice_indices_lsb0(slice(start, stop, step), length) - values2 = values_bwd[lsb0key.start : lsb0key.stop : lsb0key.step] - values2.reverse() - assert values1 == values2 - class TestCreation: def test_creation_from_bytes(self): @@ -488,121 +460,6 @@ def test_immutability_bug_prepend(self): assert c.bin == "010111" -class TestLsb0Indexing: - @classmethod - def setup_class(cls): - bitstring.lsb0 = True - - @classmethod - def teardown_class(cls): - bitstring.lsb0 = False - - def test_get_single_bit(self): - a = Bits("0b000001111") - assert a[0] is True - assert a[3] is True - assert a[4] is False - assert a[8] is False - with pytest.raises(IndexError): - _ = a[9] - assert a[-1] is False - assert a[-5] is False - assert a[-6] is True - assert a[-9] is True - with pytest.raises(IndexError): - _ = a[-10] - - def test_simple_slicing(self): - a = Bits("0xabcdef") - assert a[0:4] == "0xf" - assert a[4:8] == "0xe" - assert a[:] == "0xabcdef" - assert a[4:] == "0xabcde" - assert a[-4:] == "0xa" - assert a[-8:-4] == "0xb" - assert a[:-8] == "0xcdef" - - def test_extended_slicing(self): - a = Bits("0b0100000100100100") - assert a[2::3] == "0b10111" - - def test_all(self): - a = Bits("0b000111") - assert a.all(1, [0, 1, 2]) - assert a.all(0, [3, 4, 5]) - - def test_any(self): - a = Bits("0b00000110") - assert a.any(1, [0, 1]) - assert a.any(0, [5, 6]) - - def test_startswith(self): - a = Bits("0b0000000111") - assert a.startswith("0b111") - assert not a.startswith("0b0") - assert a.startswith("0b011", start=1) - assert not a.startswith("0b0111", end=3) - assert a.startswith("0b0111", end=4) - - def test_ends_with(self): - a = Bits("0x1234abcd") - assert a.endswith("0x123") - assert not a.endswith("0xabcd") - - def test_lsb0_slicing_error(self): - a = Bits("0b01") - b = a[::-1] - assert b == "0b10" - t = Bits("0xf0a")[::-1] - assert t == "0x50f" - s = Bits("0xf0a")[::-1][::-1] - assert s == "0xf0a" - - -class TestLsb0Interpretations: - @classmethod - def setup_class(cls): - bitstring.lsb0 = True - - @classmethod - def teardown_class(cls): - bitstring.lsb0 = False - - def test_uint(self): - a = Bits("0x01") - assert a == "0b00000001" - assert a.uint == 1 - assert a[0] is True - - def test_float(self): - a = Bits(float=0.25, length=32) - try: - bitstring.lsb0 = False - b = Bits(float=0.25, length=32) - finally: - bitstring.lsb0 = True - assert a.float == 0.25 - assert b.float == 0.25 - assert a.bin == b.bin - - def test_golomb(self): - with pytest.raises(bitstring.CreationError): - _ = Bits(ue=2) - with pytest.raises(bitstring.CreationError): - _ = Bits(se=2) - with pytest.raises(bitstring.CreationError): - _ = Bits(uie=2) - with pytest.raises(bitstring.CreationError): - _ = Bits(sie=2) - - def test_bytes(self): - a = Bits.fromstring("0xabcdef") - b = a.bytes - assert b == b"\xab\xcd\xef" - b = a.bytes3 - assert b == b"\xab\xcd\xef" - - class TestUnderscoresInLiterals: def test_hex_creation(self): a = Bits(hex="ab_cd__ef") @@ -899,28 +756,6 @@ def test_interpret_problems(self): a.pp("bin, bytes") -class TestPrettyPrinting_LSB0: - def setup_method(self) -> None: - bitstring.lsb0 = True - - def teardown_method(self) -> None: - bitstring.lsb0 = False - - def test_bin(self): - a = Bits(bin="1111 0000 0000 1111 1010") - s = io.StringIO() - a.pp("bin", stream=s, width=5) - assert ( - remove_unprintable(s.getvalue()) - == """ [ -11111010 :0 -00000000 :8 - 1111 :16 -] -""" - ) - - class TestPrettyPrinting_NewFormats: def test_float(self): a = Bits("float32=10.5") diff --git a/tests/test_bitstore.py b/tests/test_bitstore.py index d3f401d8..02281a08 100644 --- a/tests/test_bitstore.py +++ b/tests/test_bitstore.py @@ -6,7 +6,6 @@ import bitstring import sys MutableBitStore = bitstring.bitstore.MutableBitStore -offset_slice_indices_lsb0 = bitstring.helpers.offset_slice_indices_lsb0 sys.path.insert(0, '..') @@ -27,69 +26,3 @@ def test_getting_int(self): _ = a.getindex(3) with pytest.raises(IndexError): _ = a.getindex(-4) - - -class TestBasicLSB0Functionality: - - @classmethod - def setup_class(cls): - bitstring.lsb0 = True - - @classmethod - def teardown_class(cls): - bitstring.lsb0 = False - - def test_getting_int(self): - a = MutableBitStore.from_bin('001') - assert a.getindex(0) == 1 - assert a.getindex(1) == 0 - assert a.getindex(2) == 0 - - assert a.getindex(-1) == 0 - assert a.getindex(-2) == 0 - assert a.getindex(-3) == 1 - - with pytest.raises(IndexError): - _ = a.getindex(3) - with pytest.raises(IndexError): - _ = a.getindex(-4) - - def test_getting_slice(self): - a = MutableBitStore.from_bytes(b'12345678') - assert a.getslice(None, None).to_bytes() == b'12345678' - assert a.getslice(None, -8).to_bytes() == b'2345678' - assert a.getslice(8, None).to_bytes() == b'1234567' - assert a.getslice(16, 24).to_bytes() == b'6' - - def test_setting_int(self): - a = MutableBitStore.from_zeros(5) - a[0] = 1 - assert a.to_bin() == '00001' - a[-1] = 1 - assert a.to_bin() == '10001' - with pytest.raises(IndexError): - a[5] = 1 - with pytest.raises(IndexError): - a[-6] = 0 - - -class TestGettingSlices: - - def teardown_method(self) -> None: - bitstring.lsb0 = False - - def test_everything(self): - a = MutableBitStore.from_bin('010010001000110111001111101101001111') - - # Try combination of start and stop for msb0 and get the result. - # Convert to start and stop needed for lsb0 - options = [5, 2, -2, 1, 7, -3, -9, 0, -1, -len(a), len(a), len(a) - 1, -len(a) - 1, -100, 100, None] - for start_option in options: - for end_option in options: - bitstring.lsb0 = True - lsb0 = a.getslice(start_option, end_option) - bitstring.lsb0 = False - msb0 = a.getslice(start_option, end_option) - new_slice = offset_slice_indices_lsb0(slice(start_option, end_option, None), len(a)) - new_start, new_end = new_slice.start, new_slice.stop - assert len(msb0) == len(lsb0), f"[{start_option}: {end_option}] -> [{new_start}: {new_end}] len(msb0)={len(msb0)}, len(lsb0)={len(lsb0)}" diff --git a/tests/test_bitstream.py b/tests/test_bitstream.py index ddd8f47f..960f48b7 100644 --- a/tests/test_bitstream.py +++ b/tests/test_bitstream.py @@ -4021,122 +4021,6 @@ def test_bin_property(self): assert b.bin == '1010' -class TestLsb0Streaming: - - @classmethod - def setup_class(cls): - bitstring.lsb0 = True - - @classmethod - def teardown_class(cls): - bitstring.lsb0 = False - - def test_simple_bit_positions(self): - s = BitStream('0x00000f') - assert s.pos == 0 - v = s.read('uint:8') - assert v == 15 - assert s.pos == 8 - v = s.read(10) - assert v == Bits(10) - assert s.pos == 18 - - def test_bit_pos_after_find(self): - s = BitStream('0b01100001000011 0000') - s.find('0b11', start=1) - assert s.pos == 4 - - def test_iter(self): - s = BitStream('0b11000') - assert list(s) == [False, False, False, True, True] - - def test_bit_pos_after_rfind(self): - s = BitStream('0b011 000010000110000') - s.rfind('0b11') - assert s.pos == 15 - - def test_bit_pos_after_findall(self): - pass - - def test_bit_pos_after_insert(self): - pass - - def test_bit_pos_after_overwrite(self): - pass - - def test_bit_pos_after_replace(self): - pass - - def test_read_list(self): - a = BitStream('0x0123456789abcdef') - - vals = a.readlist('uint:4, uint:4, uint:24, uint:12, uint:12, uint:8') - assert vals == [15, 14, 0x89abcd, 0x567, 0x234, 1] - - -class TestLsb0PackingUnpacking: - - @classmethod - def setup_class(cls): - bitstring.lsb0 = True - - @classmethod - def teardown_class(cls): - bitstring.lsb0 = False - - def test_bin(self): - lsb0 = bitstring.pack('2*b4', '0b0000', '1111') - assert lsb0 == '0b11110000' - a, b = lsb0.unpack('2*h4') - assert [a, b] == ['0', 'f'] - a, b = lsb0.unpack('2*bits4') - assert [a, b] == ['0x0', '0xf'] - a, b = lsb0.unpack('2*bin4') - assert [a, b] == ['0000', '1111'] - - def test_float(self): - lsb0 = bitstring.pack('2*bfloat', 0.5, 15) - assert lsb0 == '0x4170 3f00' - a, b = lsb0.unpack('2*bfloat') - assert [a, b] == [0.5, 15] - - def test_simplest(self): - lsb0 = bitstring.pack('uint:2', 1) - assert lsb0.unpack('uint:2') == [1] - lsb0 = bitstring.pack('0xab, 0xc') - assert lsb0.unpack('hex8, hex4') == ['ab', 'c'] - - def test_slightly_harder(self): - lsb0 = bitstring.pack('float:32, hex', 0.25, 'ac') - x = lsb0.unpack('float:32, hex') - assert x == [0.25, 'ac'] - - def test_more_complex(self): - lsb0 = bitstring.pack('uint:10, hex, int:13, 0b11', 130, '3d', -23) - x = lsb0.unpack('uint:10, hex, int:13, bin:2') - assert x == [130, '3d', -23, '11'] - - def test_golomb_codes(self): - v = [10, 8, 6, 4, 100, -9] - # Exp-Golomb codes can only be read in msb0 mode. So also doesn't - # make sense for creation with pack - with pytest.raises(bitstring.CreationError): - _ = bitstring.pack('5*ue, sie', *v) - # with self.assertRaises(bitstring.CreationError): - # _ = BitStream('ue=34') - lsb0 = BitStream('0b0010010') - with pytest.raises(bitstring.ReadError): - _ = lsb0.unpack('5*ue, sie') - with pytest.raises(bitstring.ReadError): - _ = lsb0.read('ue') - with pytest.raises(bitstring.ReadError): - _ = lsb0.read('uie') - with pytest.raises(bitstring.ReadError): - _ = lsb0.read('se') - with pytest.raises(bitstring.ReadError): - _ = lsb0.read('sie') - - class TestRepr: def test_without_pos(self): diff --git a/tests/test_bitstring.py b/tests/test_bitstring.py index be125496..ae125006 100644 --- a/tests/test_bitstring.py +++ b/tests/test_bitstring.py @@ -22,7 +22,7 @@ class TestModuleData: def test_all(self): exported = ['ConstBitStream', 'BitStream', 'BitArray', 'Bits', 'pack', 'Error', 'ReadError', 'Array', - 'InterpretError', 'ByteAlignError', 'CreationError', 'bytealigned', 'lsb0', 'Dtype', 'options'] + 'InterpretError', 'ByteAlignError', 'CreationError', 'bytealigned', 'Dtype', 'options'] assert set(bitstring.__all__) == set(exported) def test_pyproject_version(self): @@ -85,15 +85,6 @@ def test_cbs(self): assert not a is b -class TestLSB0: - def test_getting_and_setting(self): - assert bitstring.lsb0 == False - bitstring.lsb0 = True - assert bitstring.lsb0 == True - bitstring.lsb0 = False - assert bitstring.lsb0 == False - - class TestMain: def test_running_module_directly_help(self): with redirect_stdout(io.StringIO()) as f: diff --git a/tests/test_constbitstream.py b/tests/test_constbitstream.py index ef15cca8..af25dd3d 100644 --- a/tests/test_constbitstream.py +++ b/tests/test_constbitstream.py @@ -160,39 +160,6 @@ def test_read_list_bits(self): assert [x.uint for x in v] == [3, 0, 999] -class TestLsb0Reading: - - @classmethod - def setup_class(cls): - bitstring.lsb0 = True - - @classmethod - def teardown_class(cls): - bitstring.lsb0 = False - - def test_reading_hex(self): - s = CBS('0xabcdef') - assert s.read('hex:4') == 'f' - assert s.read(4) == '0xe' - assert s.pos == 8 - - def test_reading_oct(self): - s = CBS('0o123456') - assert s.read('o6') == '56' - assert s.pos == 6 - - def test_reading_bin(self): - s = CBS('0b00011') - assert s.read('bin:3') == '011' - assert s.pos == 3 - - def test_reading_bytes(self): - s = CBS(bytes=b'54321') - assert s.pos == 0 - s.pos = 8 - assert s.read('bytes:2') == b'32' - - class TestBytesIOCreation: def test_simple_creation(self): From 49a06f44659b3933b3a7f3f7a6cf613787095875 Mon Sep 17 00:00:00 2001 From: Scott Griffiths Date: Tue, 5 May 2026 08:58:45 +0100 Subject: [PATCH 18/35] Fix to use find_all_iter. --- bitstring/bits.py | 1 + bitstring/bitstore_tibs.py | 10 +++++----- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/bitstring/bits.py b/bitstring/bits.py index 9c04f3ad..8c9aab1f 100644 --- a/bitstring/bits.py +++ b/bitstring/bits.py @@ -1287,6 +1287,7 @@ def split(self, delimiter: BitsType, start: Optional[int] = None, end: Optional[ Raises ValueError if the delimiter is empty. """ + # TODO: Delegate to Tibs.chunks_iter delimiter = Bits._create_from_bitstype(delimiter) if len(delimiter) == 0: raise ValueError("split delimiter cannot be empty.") diff --git a/bitstring/bitstore_tibs.py b/bitstring/bitstore_tibs.py index 7cf83236..0fb5f1f3 100644 --- a/bitstring/bitstore_tibs.py +++ b/bitstring/bitstore_tibs.py @@ -3,7 +3,7 @@ from tibs import Tibs, Mutibs from bitstring.exceptions import CreationError -from typing import Union, Iterable, Optional, overload, Iterator, Any +from typing import Union, Iterable, Optional, Iterator, Any def _fallback_to_u(tibs: Tibs | Mutibs) -> int: @@ -129,10 +129,10 @@ def rfind(self, bs: ConstBitStore, start: int, end: int, bytealigned: bool = Fal return self.tibs.rfind(bs.tibs, start, end, byte_aligned=bytealigned) def findall(self, bs: ConstBitStore, start: int, end: int, bytealigned: bool = False) -> Iterator[int]: - return self.tibs.find_all(bs.tibs, start=start, end=end, byte_aligned=bytealigned) + return self.tibs.find_all_iter(bs.tibs, start=start, end=end, byte_aligned=bytealigned) def rfindall(self, bs: ConstBitStore, start: int, end: int, bytealigned: bool = False) -> Iterator[int]: - return self.tibs.rfind_all(bs.tibs, start=start, end=end, byte_aligned=bytealigned) + return self.tibs.rfind_all_iter(bs.tibs, start=start, end=end, byte_aligned=bytealigned) def __iter__(self) -> Iterable[bool]: return self.tibs.__iter__() @@ -293,10 +293,10 @@ def rfind(self, bs: MutableBitStore, start: int, end: int, bytealigned: bool = F return self.tibs.rfind(bs.tibs, start, end, byte_aligned=bytealigned) def findall(self, bs: MutableBitStore, start: int, end: int, bytealigned: bool = False) -> Iterator[int]: - return self.tibs.to_tibs().find_all(bs.tibs, start=start, end=end, byte_aligned=bytealigned) + return self.tibs.to_tibs().find_all_iter(bs.tibs, start=start, end=end, byte_aligned=bytealigned) def rfindall(self, bs: MutableBitStore, start: int, end: int, bytealigned: bool = False) -> Iterator[int]: - return self.tibs.to_tibs().rfind_all(bs.tibs, start=start, end=end, byte_aligned=bytealigned) + return self.tibs.to_tibs().rfind_all_iter(bs.tibs, start=start, end=end, byte_aligned=bytealigned) def clear(self) -> None: self.tibs.clear() From fd1a31bfe695f09a0df7772887f11a857b900d85 Mon Sep 17 00:00:00 2001 From: Scott Griffiths Date: Tue, 5 May 2026 09:04:38 +0100 Subject: [PATCH 19/35] Removing deprecated way of setting module options. --- bitstring/__init__.py | 41 ++++++++++++++++++++--------------------- release_notes.md | 1 + tests/test_bitarray.py | 11 +++-------- 3 files changed, 24 insertions(+), 29 deletions(-) diff --git a/bitstring/__init__.py b/bitstring/__init__.py index 3a7d2da3..11959a72 100644 --- a/bitstring/__init__.py +++ b/bitstring/__init__.py @@ -70,7 +70,6 @@ from .array_ import Array from .exceptions import Error, ReadError, InterpretError, ByteAlignError, CreationError from .dtypes import DtypeDefinition, dtype_register, Dtype -import types from typing import List, Tuple, Literal from .mxfp import decompress_luts as mxfp_decompress_luts from .fp8 import decompress_luts as binary8_decompress_luts @@ -82,26 +81,26 @@ # The Options class returns a singleton. options = Options() -# These get defined properly by the module magic below. This just stops mypy complaining about them. -bytealigned = None - -# TODO: REMOVE FOR VERSION 5 - -# An opaque way of adding module level properties. Taken from https://peps.python.org/pep-0549/ -# This is now deprecated. Use the options object directly instead. -class _MyModuleType(types.ModuleType): - @property - def bytealigned(self) -> bool: - """Determines whether a number of methods default to working only on byte boundaries.""" - return options.bytealigned - - @bytealigned.setter - def bytealigned(self, value: bool) -> None: - """Determines whether a number of methods default to working only on byte boundaries.""" - options.bytealigned = value - - -sys.modules[__name__].__class__ = _MyModuleType +# # These get defined properly by the module magic below. This just stops mypy complaining about them. +# bytealigned = None + +# # TODO: REMOVE FOR VERSION 5 +# +# # An opaque way of adding module level properties. Taken from https://peps.python.org/pep-0549/ +# # This is now deprecated. Use the options object directly instead. +# class _MyModuleType(types.ModuleType): +# @property +# def bytealigned(self) -> bool: +# """Determines whether a number of methods default to working only on byte boundaries.""" +# return options.bytealigned +# +# @bytealigned.setter +# def bytealigned(self, value: bool) -> None: +# """Determines whether a number of methods default to working only on byte boundaries.""" +# options.bytealigned = value +# +# +# sys.modules[__name__].__class__ = _MyModuleType # These methods convert a bit length to the number of characters needed to print it for different interpretations. diff --git a/release_notes.md b/release_notes.md index 07b5d583..8422a7af 100644 --- a/release_notes.md +++ b/release_notes.md @@ -6,6 +6,7 @@ Breaking changes: * Removed LSB0 mode. +* Removed deprecated method of setting module options. ### March 2026: version 4.4.0 diff --git a/tests/test_bitarray.py b/tests/test_bitarray.py index b5288897..cdd08a76 100644 --- a/tests/test_bitarray.py +++ b/tests/test_bitarray.py @@ -144,12 +144,6 @@ def test_prepend_after_creation_from_data_with_offset(self): class TestByteAligned: - def test_changing_it(self): - bitstring.bytealigned = True - assert bitstring.bytealigned - bitstring.bytealigned = False - assert not bitstring.bytealigned - def test_not_byte_aligned(self): a = BitArray('0xff00ff') s = a.split('0xff') @@ -168,7 +162,8 @@ def test_not_byte_aligned(self): assert a == '0x000' def test_byte_aligned(self): - bitstring.bytealigned = True + # TODO: This really should be done with a context. + bitstring.options.bytealigned = True a = BitArray('0x00 ff 0f f') li = list(a.findall('0xff')) assert li == [8] @@ -180,7 +175,7 @@ def test_byte_aligned(self): assert s == ['0x00', '0xff0ff'] a.replace('0xff', '') assert a == '0x000ff' - bitstring.bytealigned = False + bitstring.options.bytealigned = False class TestSliceAssignment: From cfe15e62cbfdd3071afcd7bcca6daabe8137c8dc Mon Sep 17 00:00:00 2001 From: Scott Griffiths Date: Sat, 9 May 2026 16:25:32 +0100 Subject: [PATCH 20/35] Fix to return type if ilshift and irshift for MutableBitStore. --- bitstring/bitstore_tibs.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/bitstring/bitstore_tibs.py b/bitstring/bitstore_tibs.py index 0fb5f1f3..7a358b4d 100644 --- a/bitstring/bitstore_tibs.py +++ b/bitstring/bitstore_tibs.py @@ -246,11 +246,13 @@ def to_bin(self) -> str: def to_oct(self) -> str: return self.tibs.to_oct() - def __ilshift__(self, n: int, /) -> None: + def __ilshift__(self, n: int, /) -> MutableBitStore: self.tibs <<= n + return self - def __irshift__(self, n: int, /) -> None: + def __irshift__(self, n: int, /) -> MutableBitStore: self.tibs >>= n + return self def __add__(self, other: MutableBitStore, /) -> MutableBitStore: return MutableBitStore(self.tibs + other.tibs) @@ -304,6 +306,7 @@ def clear(self) -> None: def reverse(self) -> None: self.tibs.reverse() + # TODO: Should we remove iter from this mutable type? def __iter__(self) -> Iterable[bool]: for i in range(len(self)): yield self.getindex(i) From 1e0c1b219986cf4a2e074cdbc35890e986d8cc1e Mon Sep 17 00:00:00 2001 From: Scott Griffiths Date: Sat, 9 May 2026 16:32:07 +0100 Subject: [PATCH 21/35] Fix for some comparisons raising AttributeError --- bitstring/bitstore_tibs.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/bitstring/bitstore_tibs.py b/bitstring/bitstore_tibs.py index 7a358b4d..e3b57cc2 100644 --- a/bitstring/bitstore_tibs.py +++ b/bitstring/bitstore_tibs.py @@ -108,6 +108,8 @@ def __add__(self, other: ConstBitStore, /) -> ConstBitStore: return ConstBitStore(self.tibs + other.tibs) def __eq__(self, other: Any, /) -> bool: + if not isinstance(other, ConstBitStore): + return NotImplemented return self.tibs == other.tibs def __and__(self, other: ConstBitStore, /) -> ConstBitStore: @@ -262,6 +264,8 @@ def __iadd__(self, other: MutableBitStore | ConstBitStore, /) -> MutableBitStore return self def __eq__(self, other: Any, /) -> bool: + if not isinstance(other, (MutableBitStore, ConstBitStore)): + return NotImplemented return self.tibs == other.tibs def __and__(self, other: MutableBitStore, /) -> MutableBitStore: From 8919cbb419cd3612f06b916a16ed22d38b693080 Mon Sep 17 00:00:00 2001 From: Scott Griffiths Date: Sat, 9 May 2026 16:36:24 +0100 Subject: [PATCH 22/35] Allowing 0 as a factor when expanding brackets. --- bitstring/dtypes.py | 4 +++- bitstring/utils.py | 5 ++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/bitstring/dtypes.py b/bitstring/dtypes.py index 86c4ad1d..5b74a165 100644 --- a/bitstring/dtypes.py +++ b/bitstring/dtypes.py @@ -241,7 +241,9 @@ def __contains__(self, other: Any) -> bool: if not self.values: return True if self.values[-1] is Ellipsis: - return (other - self.values[0]) % (self.values[1] - self.values[0]) == 0 + start = self.values[0] + step = self.values[1] - self.values[0] + return other >= start and (other - start) % step == 0 return other in self.values def only_one_value(self) -> bool: diff --git a/bitstring/utils.py b/bitstring/utils.py index dec6180e..cb46a7e4 100644 --- a/bitstring/utils.py +++ b/bitstring/utils.py @@ -235,7 +235,10 @@ def expand_brackets(s: str) -> str: if m: factor = int(m.group('factor')) matchstart = m.start('factor') - s = s[0:matchstart] + (factor - 1) * (s[start + 1:p] + ',') + s[start + 1:p] + s[p + 1:] + if factor == 0: + s = s[0:matchstart] + s[p + 1:] + else: + s = s[0:matchstart] + (factor - 1) * (s[start + 1:p] + ',') + s[start + 1:p] + s[p + 1:] else: raise ValueError(f"Failed to parse '{s}'.") return s From 08720d0dde5a609f3171fc4904b1dee80dcaedaa Mon Sep 17 00:00:00 2001 From: Scott Griffiths Date: Sat, 9 May 2026 16:46:07 +0100 Subject: [PATCH 23/35] Fix for other correct inplace Array return types. --- bitstring/array_.py | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/bitstring/array_.py b/bitstring/array_.py index 8946e27c..42ee9542 100644 --- a/bitstring/array_.py +++ b/bitstring/array_.py @@ -575,6 +575,13 @@ def _apply_op_between_arrays(self, op, other: Array, is_comparison: bool = False new_array.data = new_data return new_array + def _apply_op_between_arrays_inplace(self, op, other: Array) -> Array: + """Apply op between Arrays and update self in place.""" + result = self._apply_op_between_arrays(op, other) + self._dtype = result._dtype + self.data = result.data + return self + @classmethod def _promotetype(cls, type1: Dtype, type2: Dtype) -> Dtype: """When combining types which one wins? @@ -618,12 +625,12 @@ def __add__(self, other: Union[int, float, Array]) -> Array: def __iadd__(self, other: Union[int, float, Array]) -> Array: if isinstance(other, Array): - return self._apply_op_between_arrays(operator.add, other) + return self._apply_op_between_arrays_inplace(operator.add, other) return self._apply_op_to_all_elements_inplace(operator.add, other) def __isub__(self, other: Union[int, float, Array]) -> Array: if isinstance(other, Array): - return self._apply_op_between_arrays(operator.sub, other) + return self._apply_op_between_arrays_inplace(operator.sub, other) return self._apply_op_to_all_elements_inplace(operator.sub, other) def __sub__(self, other: Union[int, float, Array]) -> Array: @@ -638,7 +645,7 @@ def __mul__(self, other: Union[int, float, Array]) -> Array: def __imul__(self, other: Union[int, float, Array]) -> Array: if isinstance(other, Array): - return self._apply_op_between_arrays(operator.mul, other) + return self._apply_op_between_arrays_inplace(operator.mul, other) return self._apply_op_to_all_elements_inplace(operator.mul, other) def __floordiv__(self, other: Union[int, float, Array]) -> Array: @@ -648,7 +655,7 @@ def __floordiv__(self, other: Union[int, float, Array]) -> Array: def __ifloordiv__(self, other: Union[int, float, Array]) -> Array: if isinstance(other, Array): - return self._apply_op_between_arrays(operator.floordiv, other) + return self._apply_op_between_arrays_inplace(operator.floordiv, other) return self._apply_op_to_all_elements_inplace(operator.floordiv, other) def __truediv__(self, other: Union[int, float, Array]) -> Array: @@ -658,7 +665,7 @@ def __truediv__(self, other: Union[int, float, Array]) -> Array: def __itruediv__(self, other: Union[int, float, Array]) -> Array: if isinstance(other, Array): - return self._apply_op_between_arrays(operator.truediv, other) + return self._apply_op_between_arrays_inplace(operator.truediv, other) return self._apply_op_to_all_elements_inplace(operator.truediv, other) def __rshift__(self, other: Union[int, Array]) -> Array: @@ -673,12 +680,12 @@ def __lshift__(self, other: Union[int, Array]) -> Array: def __irshift__(self, other: Union[int, Array]) -> Array: if isinstance(other, Array): - return self._apply_op_between_arrays(operator.rshift, other) + return self._apply_op_between_arrays_inplace(operator.rshift, other) return self._apply_op_to_all_elements_inplace(operator.rshift, other) def __ilshift__(self, other: Union[int, Array]) -> Array: if isinstance(other, Array): - return self._apply_op_between_arrays(operator.lshift, other) + return self._apply_op_between_arrays_inplace(operator.lshift, other) return self._apply_op_to_all_elements_inplace(operator.lshift, other) def __mod__(self, other: Union[int, Array]) -> Array: @@ -688,7 +695,7 @@ def __mod__(self, other: Union[int, Array]) -> Array: def __imod__(self, other: Union[int, Array]) -> Array: if isinstance(other, Array): - return self._apply_op_between_arrays(operator.mod, other) + return self._apply_op_between_arrays_inplace(operator.mod, other) return self._apply_op_to_all_elements_inplace(operator.mod, other) # Bitwise operators @@ -775,4 +782,4 @@ def __neg__(self): return self._apply_op_to_all_elements(operator.neg, None) def __abs__(self): - return self._apply_op_to_all_elements(operator.abs, None) \ No newline at end of file + return self._apply_op_to_all_elements(operator.abs, None) From d014786aab34c8d05a2df0651f2f854661915ce3 Mon Sep 17 00:00:00 2001 From: Scott Griffiths Date: Sat, 9 May 2026 16:47:21 +0100 Subject: [PATCH 24/35] Scale should be used in Dtype comparisons. --- bitstring/dtypes.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bitstring/dtypes.py b/bitstring/dtypes.py index 5b74a165..27be4e74 100644 --- a/bitstring/dtypes.py +++ b/bitstring/dtypes.py @@ -141,7 +141,7 @@ def _new_from_token(cls, token: str, scale: Union[None, float, int] = None) -> D return dtype_register.get_dtype(*utils.parse_name_length_token(token), scale=scale) def __hash__(self) -> int: - return hash((self._name, self._length)) + return hash((self._name, self._length, self._scale)) @classmethod @functools.lru_cache(CACHE_SIZE) @@ -217,7 +217,7 @@ def __repr__(self) -> str: def __eq__(self, other: Any) -> bool: if isinstance(other, Dtype): - return self._name == other._name and self._length == other._length + return self._name == other._name and self._length == other._length and self._scale == other._scale return False From fc9ca9562f8b2204de4df8a444b6003128da71f9 Mon Sep 17 00:00:00 2001 From: Scott Griffiths Date: Sat, 9 May 2026 16:48:33 +0100 Subject: [PATCH 25/35] Check for negative factors in tokens. --- bitstring/utils.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/bitstring/utils.py b/bitstring/utils.py index cb46a7e4..06748eda 100644 --- a/bitstring/utils.py +++ b/bitstring/utils.py @@ -156,6 +156,8 @@ def preprocess_tokens(fmt: str) -> List[str]: factor = 1 if m := MULTIPLICATIVE_RE.match(meta_token): factor = int(m.group('factor')) + if factor < 0: + raise ValueError(f"Negative multiplicative factors are not allowed: '{meta_token}'.") meta_token = m.group('token') # Parse struct-like format into sub-tokens or treat as single token From 21e576465a9f3441e2594bcb3519b0341e0f23c8 Mon Sep 17 00:00:00 2001 From: Scott Griffiths Date: Sat, 9 May 2026 16:50:35 +0100 Subject: [PATCH 26/35] Don't let reinitialising the Options reset them. --- bitstring/bitstring_options.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/bitstring/bitstring_options.py b/bitstring/bitstring_options.py index d3d1976c..8b9edc22 100644 --- a/bitstring/bitstring_options.py +++ b/bitstring/bitstring_options.py @@ -12,12 +12,15 @@ class Options: _instance = None def __init__(self): + if hasattr(self, "_initialised"): + return self._bytealigned = False self.mxfp_overflow = 'saturate' self.no_color = False no_color = os.getenv('NO_COLOR') self.no_color = True if no_color else False + self._initialised = True @property def mxfp_overflow(self) -> str: From 38a20a1db997f41cdd651d6b20cda963c7a6b7fc Mon Sep 17 00:00:00 2001 From: Scott Griffiths Date: Sat, 9 May 2026 16:55:51 +0100 Subject: [PATCH 27/35] Better array insert clamping. --- bitstring/array_.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bitstring/array_.py b/bitstring/array_.py index 42ee9542..8aff3c9e 100644 --- a/bitstring/array_.py +++ b/bitstring/array_.py @@ -311,7 +311,8 @@ def insert(self, i: int, x: ElementType) -> None: """Insert a new element into the Array at position i. """ - i = min(i, len(self)) # Inserting beyond len of array inserts at the end (copying standard behaviour) + # Match list.insert semantics: clamp both high and low indices. + i = max(min(i, len(self)), -len(self)) self.data.insert(self._create_element(x), i * self._dtype.length) def pop(self, i: int = -1) -> ElementType: From 38b0156f4e5f7507938895c06b9961d92e04f7d8 Mon Sep 17 00:00:00 2001 From: Scott Griffiths Date: Sat, 9 May 2026 16:58:34 +0100 Subject: [PATCH 28/35] A few fixes for Array.fromfile, plus some AI generated unit tests. --- bitstring/array_.py | 14 ++- tests/test_bits.py | 2 +- tests/test_generated.py | 228 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 240 insertions(+), 4 deletions(-) create mode 100644 tests/test_generated.py diff --git a/bitstring/array_.py b/bitstring/array_.py index 8aff3c9e..ede54f41 100644 --- a/bitstring/array_.py +++ b/bitstring/array_.py @@ -371,11 +371,19 @@ def fromfile(self, f: BinaryIO, n: Optional[int] = None) -> None: trailing_bit_length = len(self.data) % self._dtype.bitlength if trailing_bit_length != 0: raise ValueError(f"Cannot extend Array as its data length ({len(self.data)} bits) is not a multiple of the format length ({self._dtype.bitlength} bits).") + if n is not None and n < 0: + raise ValueError("n must be >= 0.") - new_data = Bits(f) - max_items = len(new_data) // self._dtype.length + item_bits = self._dtype.bitlength + if n is None: + b = f.read() + else: + bytes_needed = (n * item_bits + 7) // 8 + b = f.read(bytes_needed) + new_data = Bits(b) + max_items = len(new_data) // item_bits items_to_append = max_items if n is None else min(n, max_items) - self.data += new_data[0: items_to_append * self._dtype.bitlength] + self.data += new_data[0: items_to_append * item_bits] if n is not None and items_to_append < n: raise EOFError(f"Only {items_to_append} were appended, not the {n} items requested.") diff --git a/tests/test_bits.py b/tests/test_bits.py index faba9665..71c18c18 100644 --- a/tests/test_bits.py +++ b/tests/test_bits.py @@ -7,7 +7,7 @@ import os import re from bitstring import InterpretError, Bits, BitArray -from hypothesis import given, settings +from hypothesis import given import hypothesis.strategies as st sys.path.insert(0, "..") diff --git a/tests/test_generated.py b/tests/test_generated.py new file mode 100644 index 00000000..797c5ea0 --- /dev/null +++ b/tests/test_generated.py @@ -0,0 +1,228 @@ +# LLM generated test cases +import bitstring + + +ConstBitStore = bitstring.bitstore.ConstBitStore +MutableBitStore = bitstring.bitstore.MutableBitStore + + +def test_mutable_bitstore_ilshift_keeps_binding_and_returns_self() -> None: + bs = MutableBitStore.from_bin("1011") + original_id = id(bs) + + bs <<= 1 + + assert id(bs) == original_id + assert bs.to_bin() == "0110" + + +def test_mutable_bitstore_irshift_keeps_binding_and_returns_self() -> None: + bs = MutableBitStore.from_bin("1011") + original_id = id(bs) + + bs >>= 1 + + assert id(bs) == original_id + assert bs.to_bin() == "0101" + + +def test_const_bitstore_eq_non_bitstore_returns_false() -> None: + bs = ConstBitStore.from_bin("1") + assert (bs == 1) is False + + +def test_mutable_bitstore_eq_non_bitstore_returns_false() -> None: + bs = MutableBitStore.from_bin("1") + assert (bs == 1) is False + + +def test_dtype_scaled_instances_compare_distinct() -> None: + from bitstring.dtypes import Dtype + + a = Dtype("uint8", scale=2) + b = Dtype("uint8", scale=3) + + assert a != b + assert len({a, b}) == 2 + + +def test_dtype_uintbe_zero_length_rejected() -> None: + from bitstring.dtypes import Dtype + import pytest + + with pytest.raises(ValueError): + _ = Dtype("uintbe0") + + +def test_array_fromfile_reads_from_current_file_position(tmp_path) -> None: + from bitstring.array_ import Array + + p = tmp_path / "fromfile.bin" + p.write_bytes(bytes([1, 2, 3, 4])) + + with p.open("rb") as f: + f.seek(1) + a = Array("uint8") + a.fromfile(f, 2) + + assert a.tolist() == [2, 3] + + +def test_pack_with_zero_repeated_group_needs_no_values() -> None: + s = bitstring.pack("0*(uint:8)") + assert len(s) == 0 + + +def test_array_iadd_with_array_is_in_place() -> None: + from bitstring.array_ import Array + + a = Array("int8", [1, 2]) + alias = a + a += Array("int8", [3, 4]) + + assert a is alias + assert alias.tolist() == [4, 6] + + +def test_array_fromfile_negative_n_rejected(tmp_path) -> None: + from bitstring.array_ import Array + import pytest + + p = tmp_path / "fromfile_negative.bin" + p.write_bytes(bytes([1, 2, 3])) + + with p.open("rb") as f: + a = Array("uint8") + with pytest.raises(ValueError): + a.fromfile(f, -1) + + +def test_dtype_uintbe_negative_length_rejected() -> None: + from bitstring.dtypes import Dtype + import pytest + + with pytest.raises(ValueError): + _ = Dtype("uintbe", -8) + + +def test_pack_negative_repeat_factor_rejected() -> None: + import pytest + + with pytest.raises(ValueError): + _ = bitstring.pack("-2*uint:8") + + +def test_options_constructor_does_not_reset_singleton_state() -> None: + from bitstring.bitstring_options import Options + + old_bytealigned = bitstring.options.bytealigned + old_overflow = bitstring.options.mxfp_overflow + try: + bitstring.options.bytealigned = True + bitstring.options.mxfp_overflow = "overflow" + Options() + assert bitstring.options.bytealigned is True + assert bitstring.options.mxfp_overflow == "overflow" + finally: + bitstring.options.bytealigned = old_bytealigned + bitstring.options.mxfp_overflow = old_overflow + + +def test_array_insert_very_negative_index_clamps_to_start() -> None: + from bitstring.array_ import Array + + a = Array("uint8", [10, 20, 30]) + a.insert(-100, 5) + assert a.tolist() == [5, 10, 20, 30] + + +def test_array_isub_with_array_is_in_place() -> None: + from bitstring.array_ import Array + + a = Array("int8", [5, 7]) + alias = a + a -= Array("int8", [2, 3]) + + assert a is alias + assert alias.tolist() == [3, 4] + + +def test_array_imul_with_array_is_in_place() -> None: + from bitstring.array_ import Array + + a = Array("int8", [2, 3]) + alias = a + a *= Array("int8", [4, 5]) + + assert a is alias + assert alias.tolist() == [8, 15] + + +def test_array_ifloordiv_with_array_is_in_place() -> None: + from bitstring.array_ import Array + + a = Array("int8", [8, 15]) + alias = a + a //= Array("int8", [2, 5]) + + assert a is alias + assert alias.tolist() == [4, 3] + + +def test_array_itruediv_with_array_is_in_place() -> None: + from bitstring.array_ import Array + + a = Array("float16", [8.0, 15.0]) + alias = a + a /= Array("float16", [2.0, 5.0]) + + assert a is alias + assert alias.tolist() == [4.0, 3.0] + + +def test_array_ilshift_with_array_is_in_place() -> None: + from bitstring.array_ import Array + + a = Array("uint8", [1, 2]) + alias = a + a <<= Array("uint8", [3, 2]) + + assert a is alias + assert alias.tolist() == [8, 8] + + +def test_array_irshift_with_array_is_in_place() -> None: + from bitstring.array_ import Array + + a = Array("uint8", [16, 18]) + alias = a + a >>= Array("uint8", [3, 1]) + + assert a is alias + assert alias.tolist() == [2, 9] + + +def test_array_imod_with_array_is_in_place() -> None: + from bitstring.array_ import Array + + a = Array("uint8", [20, 19]) + alias = a + a %= Array("uint8", [6, 7]) + + assert a is alias + assert alias.tolist() == [2, 5] + + +def test_array_fromfile_honours_current_position_for_eof(tmp_path) -> None: + from bitstring.array_ import Array + import pytest + + p = tmp_path / "fromfile_eof.bin" + p.write_bytes(bytes([1, 2, 3])) + + with p.open("rb") as f: + f.seek(2) + a = Array("uint8") + with pytest.raises(EOFError): + a.fromfile(f, 2) + assert a.tolist() == [3] From b2953484045d98c6e77e03950a0a5d2d7134a2b5 Mon Sep 17 00:00:00 2001 From: Scott Griffiths Date: Sat, 9 May 2026 17:19:37 +0100 Subject: [PATCH 29/35] Using offset and length of Tibs.from_bytes to improve performance. --- bitstring/bits.py | 13 +++++-------- bitstring/bitstore_helpers.py | 5 ++--- bitstring/bitstore_tibs.py | 20 +++++++++++--------- 3 files changed, 18 insertions(+), 20 deletions(-) diff --git a/bitstring/bits.py b/bitstring/bits.py index 8c9aab1f..3e12be82 100644 --- a/bitstring/bits.py +++ b/bitstring/bits.py @@ -525,14 +525,12 @@ def _setauto(self, s: BitsType, length: Optional[int], offset: Optional[int], /) offset = 0 if isinstance(s, io.BytesIO): + total_bits = s.seek(0, 2) * 8 if length is None: - length = s.seek(0, 2) * 8 - offset - byteoffset, offset = divmod(offset, 8) - bytelength = (length + byteoffset * 8 + offset + 7) // 8 - byteoffset - if length + byteoffset * 8 + offset > s.seek(0, 2) * 8: + length = total_bits - offset + if length + offset > total_bits: raise bitstring.CreationError("BytesIO object is not long enough for specified length and offset.") - self._bitstore = ConstBitStore.from_bytes(s.getvalue()[byteoffset: byteoffset + bytelength]).getslice( - offset, offset + length) + self._bitstore = ConstBitStore.from_bytes(s.getvalue(), offset=offset, length=length) return if isinstance(s, io.BufferedReader): @@ -612,7 +610,7 @@ def _setbytes_with_truncation(self, data: Union[bytearray, bytes], length: Optio else: if length + offset > len(data) * 8: raise bitstring.CreationError(f"Not enough data present. Need {length + offset} bits, have {len(data) * 8}.") - self._bitstore = ConstBitStore.from_bytes(data).getslice(offset, offset + length) + self._bitstore = ConstBitStore.from_bytes(data, offset=offset, length=length) def _getbytes(self) -> bytes: """Return the data as an ordinary bytes object.""" @@ -1626,4 +1624,3 @@ def fromstring(cls: TBits, s: str, /) -> TBits: len = length = property(_getlength, doc="The length of the bitstring in bits. Read only.") - diff --git a/bitstring/bitstore_helpers.py b/bitstring/bitstore_helpers.py index f89e0fdb..5fb54033 100644 --- a/bitstring/bitstore_helpers.py +++ b/bitstring/bitstore_helpers.py @@ -29,8 +29,7 @@ def _int_to_mutibs(i: int, length: int, signed: bool, little_endian: bool) -> Mu byteorder = "little" if little_endian else "big" b = i.to_bytes((length + 7) // 8, byteorder=byteorder, signed=signed) offset = (-length) % 8 - mb = Mutibs.from_bytes(b) - return mb if offset == 0 else mb[offset:] + return Mutibs.from_bytes(b, offset=offset, length=length) def bin2bitstore(binstring: str) -> MutableBitStore: @@ -247,4 +246,4 @@ def mxint2bitstore(f: Union[str, float]) -> MutableBitStore: i = int(f) if f - i == 0.0 and i % 2: i += 1 - return int2bitstore(i, 8, True) \ No newline at end of file + return int2bitstore(i, 8, True) diff --git a/bitstring/bitstore_tibs.py b/bitstring/bitstore_tibs.py index e3b57cc2..1c410a91 100644 --- a/bitstring/bitstore_tibs.py +++ b/bitstring/bitstore_tibs.py @@ -48,9 +48,10 @@ def from_zeros(cls, i: int): return x @classmethod - def from_bytes(cls, b: Union[bytes, bytearray, memoryview], /) -> ConstBitStore: + def from_bytes(cls, b: Union[bytes, bytearray, memoryview], /, offset: Optional[int] = None, + length: Optional[int] = None) -> ConstBitStore: x = super().__new__(cls) - x.tibs = Tibs.from_bytes(b) + x.tibs = Tibs.from_bytes(b, offset=offset, length=length) return x @classmethod @@ -61,15 +62,15 @@ def from_bools(cls, iterable: Iterable[Any], /) -> ConstBitStore: @classmethod def frombuffer(cls, buffer, /, length: Optional[int] = None) -> ConstBitStore: #TODO: Shouldn't need a default here. - x = super().__new__(cls) - x.tibs = Tibs.from_bytes(memoryview(buffer)) + mv = memoryview(buffer) if length is not None: if length < 0: raise CreationError("Can't create bitstring with a negative length.") - if length > len(x.tibs): + if length > mv.nbytes * 8: raise CreationError( - f"Can't create bitstring with a length of {length} from {len(x.tibs)} bits of data.") - return x.getslice(0, length) if length is not None else x + f"Can't create bitstring with a length of {length} from {mv.nbytes * 8} bits of data.") + return cls.from_bytes(mv, length=length) + return cls.from_bytes(mv) @classmethod def from_bin(cls, s: str) -> ConstBitStore: @@ -199,9 +200,10 @@ def from_zeros(cls, i: int): return x @classmethod - def from_bytes(cls, b: Union[bytes, bytearray, memoryview], /) -> MutableBitStore: + def from_bytes(cls, b: Union[bytes, bytearray, memoryview], /, offset: Optional[int] = None, + length: Optional[int] = None) -> MutableBitStore: x = super().__new__(cls) - x.tibs = Mutibs.from_bytes(b) + x.tibs = Mutibs.from_bytes(b, offset=offset, length=length) return x @classmethod From 5b7f1444f8d35820ef574d982c4952f6cbf5db8d Mon Sep 17 00:00:00 2001 From: Scott Griffiths Date: Sat, 9 May 2026 18:05:20 +0100 Subject: [PATCH 30/35] Don't always read file into memory if offset given. --- bitstring/bits.py | 17 ++++++++--------- bitstring/bitstore_tibs.py | 17 ++++++++++++----- 2 files changed, 20 insertions(+), 14 deletions(-) diff --git a/bitstring/bits.py b/bitstring/bits.py index 3e12be82..b0e42f7b 100644 --- a/bitstring/bits.py +++ b/bitstring/bits.py @@ -548,20 +548,20 @@ def _setfile(self, filename: str, length: Optional[int] = None, offset: Optional if offset is None: offset = 0 m = mmap.mmap(source.fileno(), 0, access=mmap.ACCESS_READ) + file_bits = len(m) * 8 if offset == 0: self._filename = source.name self._bitstore = ConstBitStore.frombuffer(m, length=length) else: - # If offset is given then always read into memory. - temp = ConstBitStore.frombuffer(m) if length is None: - if offset > len(temp): - raise bitstring.CreationError(f"The offset of {offset} bits is greater than the file length ({len(temp)} bits).") - self._bitstore = temp.getslice(offset, None) + if offset > file_bits: + raise bitstring.CreationError(f"The offset of {offset} bits is greater than the file length ({file_bits} bits).") + self._bitstore = ConstBitStore.frombuffer(m, offset=offset) else: - self._bitstore = temp.getslice(offset, offset + length) - if len(self) != length: - raise bitstring.CreationError(f"Can't use a length of {length} bits and an offset of {offset} bits as file length is only {len(temp)} bits.") + if offset + length > file_bits: + raise bitstring.CreationError( + f"Can't use a length of {length} bits and an offset of {offset} bits as file length is only {file_bits} bits.") + self._bitstore = ConstBitStore.frombuffer(m, offset=offset, length=length) def _setbits(self, bs: BitsType, length: None = None) -> None: bs = Bits._create_from_bitstype(bs) @@ -1623,4 +1623,3 @@ def fromstring(cls: TBits, s: str, /) -> TBits: return x len = length = property(_getlength, doc="The length of the bitstring in bits. Read only.") - diff --git a/bitstring/bitstore_tibs.py b/bitstring/bitstore_tibs.py index 1c410a91..af1ffb37 100644 --- a/bitstring/bitstore_tibs.py +++ b/bitstring/bitstore_tibs.py @@ -61,16 +61,23 @@ def from_bools(cls, iterable: Iterable[Any], /) -> ConstBitStore: return x @classmethod - def frombuffer(cls, buffer, /, length: Optional[int] = None) -> ConstBitStore: #TODO: Shouldn't need a default here. + def frombuffer(cls, buffer, /, length: Optional[int] = None, offset: Optional[int] = None) -> ConstBitStore: #TODO: Shouldn't need a default here. mv = memoryview(buffer) + if offset is None: + offset = 0 + if offset < 0: + raise CreationError("Can't create bitstring with a negative offset.") + if offset > mv.nbytes * 8: + raise CreationError( + f"Can't create bitstring with an offset of {offset} from {mv.nbytes * 8} bits of data.") if length is not None: if length < 0: raise CreationError("Can't create bitstring with a negative length.") - if length > mv.nbytes * 8: + if offset + length > mv.nbytes * 8: raise CreationError( - f"Can't create bitstring with a length of {length} from {mv.nbytes * 8} bits of data.") - return cls.from_bytes(mv, length=length) - return cls.from_bytes(mv) + f"Can't create bitstring with a length of {length} from {mv.nbytes * 8 - offset} bits of data.") + return cls.from_bytes(mv, offset=offset, length=length) + return cls.from_bytes(mv, offset=offset) @classmethod def from_bin(cls, s: str) -> ConstBitStore: From 12170fcc7ff00d3f048ff05a5d7bc5e1514dfc38 Mon Sep 17 00:00:00 2001 From: Scott Griffiths Date: Sat, 9 May 2026 18:27:27 +0100 Subject: [PATCH 31/35] Removing deprecated old options. --- bitstring/__init__.py | 23 +---------------------- 1 file changed, 1 insertion(+), 22 deletions(-) diff --git a/bitstring/__init__.py b/bitstring/__init__.py index 11959a72..71953e71 100644 --- a/bitstring/__init__.py +++ b/bitstring/__init__.py @@ -81,27 +81,6 @@ # The Options class returns a singleton. options = Options() -# # These get defined properly by the module magic below. This just stops mypy complaining about them. -# bytealigned = None - -# # TODO: REMOVE FOR VERSION 5 -# -# # An opaque way of adding module level properties. Taken from https://peps.python.org/pep-0549/ -# # This is now deprecated. Use the options object directly instead. -# class _MyModuleType(types.ModuleType): -# @property -# def bytealigned(self) -> bool: -# """Determines whether a number of methods default to working only on byte boundaries.""" -# return options.bytealigned -# -# @bytealigned.setter -# def bytealigned(self, value: bool) -> None: -# """Determines whether a number of methods default to working only on byte boundaries.""" -# options.bytealigned = value -# -# -# sys.modules[__name__].__class__ = _MyModuleType - # These methods convert a bit length to the number of characters needed to print it for different interpretations. def hex_bits2chars(bitlength: int): @@ -325,4 +304,4 @@ def bool_bits2chars(_: Literal[1]): __all__ = ['ConstBitStream', 'BitStream', 'BitArray', 'Array', 'Bits', 'pack', 'Error', 'ReadError', 'InterpretError', - 'ByteAlignError', 'CreationError', 'bytealigned', 'Dtype', 'options'] + 'ByteAlignError', 'CreationError', 'Dtype', 'options'] From 7456cfc038e629c16a00435f643c8c53e47f62c4 Mon Sep 17 00:00:00 2001 From: Scott Griffiths Date: Sat, 9 May 2026 18:35:28 +0100 Subject: [PATCH 32/35] Fix to make ConstBitStore the default. Surprisingly big speed up :) --- bitstring/bits.py | 5 +++- bitstring/bitstore_helpers.py | 48 +++++++++++++++++++++++++++-------- bitstring/bitstream.py | 2 +- tests/test_bitstring.py | 2 +- 4 files changed, 43 insertions(+), 14 deletions(-) diff --git a/bitstring/bits.py b/bitstring/bits.py index b0e42f7b..a4e9b9f1 100644 --- a/bitstring/bits.py +++ b/bitstring/bits.py @@ -161,7 +161,10 @@ def _initialise(self, auto: Any, /, length: Optional[int], offset: Optional[int] Dtype(k, length).set_fn(self, v) except ValueError as e: raise bitstring.CreationError(e) - if not immutable: + if immutable: + if isinstance(self._bitstore, MutableBitStore): + self._bitstore = ConstBitStore(self._bitstore.tibs.to_tibs()) + else: # TODO: This copy is not a good idea. self._bitstore = self._bitstore._mutable_copy() diff --git a/bitstring/bitstore_helpers.py b/bitstring/bitstore_helpers.py index 5fb54033..52acfc52 100644 --- a/bitstring/bitstore_helpers.py +++ b/bitstring/bitstore_helpers.py @@ -1,6 +1,6 @@ from __future__ import annotations -from tibs import Mutibs, Endianness +from tibs import Tibs, Mutibs, Endianness import struct import math @@ -13,6 +13,7 @@ MutableBitStore = bitstring.bitstore.MutableBitStore +ConstBitStore = bitstring.bitstore.ConstBitStore from bitstring.helpers import tidy_input_string @@ -73,24 +74,48 @@ def float2bitstore(f: Union[str, float], length: int, big_endian: bool) -> Mutab CACHE_SIZE = 256 +def _to_const_bitstore(bs: MutableBitStore | ConstBitStore) -> ConstBitStore: + if isinstance(bs, ConstBitStore): + return bs + return ConstBitStore(bs.tibs.to_tibs()) + + +def _bin_literal_to_const_bitstore(binstring: str) -> ConstBitStore: + binstring = tidy_input_string(binstring) + binstring = binstring.replace('0b', '') + return ConstBitStore.from_bin(binstring) + + +def _hex_literal_to_const_bitstore(hexstring: str) -> ConstBitStore: + hexstring = tidy_input_string(hexstring) + hexstring = hexstring.replace('0x', '') + return ConstBitStore(Tibs.from_hex(hexstring)) + + +def _oct_literal_to_const_bitstore(octstring: str) -> ConstBitStore: + octstring = tidy_input_string(octstring) + octstring = octstring.replace('0o', '') + return ConstBitStore(Tibs.from_oct(octstring)) + + @functools.lru_cache(CACHE_SIZE) -def str_to_bitstore(s: str) -> MutableBitStore: +def str_to_bitstore(s: str) -> ConstBitStore: _, tokens = bitstring.utils.tokenparser(s) constbitstores = [bitstore_from_token(*token) for token in tokens] - return MutableBitStore.join(constbitstores) + return ConstBitStore.join(constbitstores) -literal_bit_funcs: Dict[str, Callable[..., MutableBitStore]] = { - '0x': hex2bitstore, - '0X': hex2bitstore, - '0b': bin2bitstore, - '0B': bin2bitstore, - '0o': oct2bitstore, - '0O': oct2bitstore, +literal_bit_funcs: Dict[str, Callable[..., ConstBitStore]] = { + '0x': _hex_literal_to_const_bitstore, + '0X': _hex_literal_to_const_bitstore, + '0b': _bin_literal_to_const_bitstore, + '0B': _bin_literal_to_const_bitstore, + '0o': _oct_literal_to_const_bitstore, + '0O': _oct_literal_to_const_bitstore, } -def bitstore_from_token(name: str, token_length: Optional[int], value: Optional[str]) -> MutableBitStore: +def bitstore_from_token(name: str, token_length: Optional[int], value: Optional[str]) -> ConstBitStore: if name in literal_bit_funcs: return literal_bit_funcs[name](value) try: @@ -100,6 +125,7 @@ def bitstore_from_token(name: str, token_length: Optional[int], value: Optional[ if value is None and name != 'pad': raise ValueError(f"Token {name} requires a value.") bs = d.build(value)._bitstore + bs = _to_const_bitstore(bs) if token_length is not None and len(bs) != d.bitlength: raise bitstring.CreationError(f"Token with length {token_length} packed with value of length {len(bs)} " f"({name}:{token_length}={value}).") diff --git a/bitstring/bitstream.py b/bitstring/bitstream.py index 7ed6ed40..de7f6f73 100644 --- a/bitstring/bitstream.py +++ b/bitstring/bitstream.py @@ -721,4 +721,4 @@ def replace(self, old: BitsType, new: BitsType, start: Optional[int] = None, end replacement_count = self._replace(old, new, start, end, 0 if count is None else count, bytealigned) if len(self) != length_before: self._pos = 0 - return replacement_count \ No newline at end of file + return replacement_count diff --git a/tests/test_bitstring.py b/tests/test_bitstring.py index ae125006..59caa165 100644 --- a/tests/test_bitstring.py +++ b/tests/test_bitstring.py @@ -22,7 +22,7 @@ class TestModuleData: def test_all(self): exported = ['ConstBitStream', 'BitStream', 'BitArray', 'Bits', 'pack', 'Error', 'ReadError', 'Array', - 'InterpretError', 'ByteAlignError', 'CreationError', 'bytealigned', 'Dtype', 'options'] + 'InterpretError', 'ByteAlignError', 'CreationError', 'Dtype', 'options'] assert set(bitstring.__all__) == set(exported) def test_pyproject_version(self): From 8d7b4de3a73ede89a21040f3282ed5ffdfc69e55 Mon Sep 17 00:00:00 2001 From: Scott Griffiths Date: Sat, 9 May 2026 18:47:47 +0100 Subject: [PATCH 33/35] More ConstBitStore correctness. --- bitstring/bitarray_.py | 2 +- bitstring/bits.py | 53 +++++++++++++++---------- bitstring/bitstore_helpers.py | 75 +++++++++++++++++------------------ 3 files changed, 69 insertions(+), 61 deletions(-) diff --git a/bitstring/bitarray_.py b/bitstring/bitarray_.py index 3b094a10..2d616b43 100644 --- a/bitstring/bitarray_.py +++ b/bitstring/bitarray_.py @@ -155,7 +155,7 @@ def __setattr__(self, attribute, value) -> None: if len(x) != dtype.bitlength: raise CreationError(f"Can't initialise with value of length {len(x)} bits, " f"as attribute has length of {dtype.bitlength} bits.") - self._bitstore = x._bitstore + self._bitstore = x._bitstore._mutable_copy() return def __iadd__(self, bs: BitsType) -> BitArray: diff --git a/bitstring/bits.py b/bitstring/bits.py index a4e9b9f1..8746c672 100644 --- a/bitstring/bits.py +++ b/bitstring/bits.py @@ -207,11 +207,8 @@ def __add__(self: TBits, bs: BitsType) -> TBits: """ bs = self.__class__._create_from_bitstype(bs) - s = self._copy() if len(bs) <= len(self) else bs._copy() - if len(bs) <= len(self): - s._addright(bs) - else: - s._addleft(self) + s = super().__new__(self.__class__) + s._bitstore = self._bitstore + bs._bitstore return s def __radd__(self: TBits, bs: BitsType) -> TBits: @@ -973,7 +970,10 @@ def _copy(self: TBits) -> TBits: """Create and return a new copy of the Bits (always in memory).""" # Note that __copy__ may choose to return self if it's immutable. This method always makes a copy. s_copy = self.__class__() - s_copy._bitstore = self._bitstore._mutable_copy() + if isinstance(self._bitstore, ConstBitStore): + s_copy._bitstore = ConstBitStore(self._bitstore.tibs) + else: + s_copy._bitstore = self._bitstore._mutable_copy() return s_copy def _slice(self: TBits, start: int, end: int) -> TBits: @@ -1009,11 +1009,17 @@ def _readtoken(self, name: str, pos: int, length: Optional[int]) -> Tuple[Union[ def _addright(self, bs: Bits, /) -> None: """Add a bitstring to the RHS of the current bitstring.""" - self._bitstore += bs._bitstore + if isinstance(self._bitstore, ConstBitStore): + self._bitstore = self._bitstore + bs._bitstore + else: + self._bitstore += bs._bitstore def _addleft(self, bs: Bits, /) -> None: """Prepend a bitstring to the current bitstring.""" - self._bitstore.extend_left(bs._bitstore) + if isinstance(self._bitstore, ConstBitStore): + self._bitstore = bs._bitstore + self._bitstore + else: + self._bitstore.extend_left(bs._bitstore) def _insert(self, bs: Bits, pos: int, /) -> None: """Insert bs at pos.""" @@ -1327,23 +1333,28 @@ def join(self: TBits, sequence: Iterable[Any]) -> TBits: sequence -- A sequence of bitstrings. """ - bs = MutableBitStore.from_zeros(0) - if len(self) == 0: - # Optimised version that doesn't need to add self between every item - for item in sequence: - bs += Bits._create_from_bitstype(item)._bitstore - else: + def _stores() -> Iterable[ConstBitStore | MutableBitStore]: + if len(self) == 0: + # Optimised version that doesn't need to add self between every item + for item in sequence: + yield Bits._create_from_bitstype(item)._bitstore + return + sequence_iter = iter(sequence) try: - bs += Bits._create_from_bitstype(next(sequence_iter))._bitstore + first = Bits._create_from_bitstype(next(sequence_iter))._bitstore except StopIteration: - pass - else: - for item in sequence_iter: - bs += self._bitstore - bs += Bits._create_from_bitstype(item)._bitstore + return + yield first + for item in sequence_iter: + yield self._bitstore + yield Bits._create_from_bitstype(item)._bitstore + s = self.__class__() - s._bitstore = bs + if isinstance(self._bitstore, ConstBitStore): + s._bitstore = ConstBitStore.join(_stores()) + else: + s._bitstore = MutableBitStore.join(_stores()) return s def tobytes(self) -> bytes: diff --git a/bitstring/bitstore_helpers.py b/bitstring/bitstore_helpers.py index 52acfc52..e16d59eb 100644 --- a/bitstring/bitstore_helpers.py +++ b/bitstring/bitstore_helpers.py @@ -33,43 +33,40 @@ def _int_to_mutibs(i: int, length: int, signed: bool, little_endian: bool) -> Mu return Mutibs.from_bytes(b, offset=offset, length=length) -def bin2bitstore(binstring: str) -> MutableBitStore: +def bin2bitstore(binstring: str) -> ConstBitStore: binstring = tidy_input_string(binstring) binstring = binstring.replace('0b', '') - mb = Mutibs.from_bin(binstring) - return MutableBitStore(mb) + return ConstBitStore.from_bin(binstring) -def hex2bitstore(hexstring: str) -> MutableBitStore: +def hex2bitstore(hexstring: str) -> ConstBitStore: hexstring = tidy_input_string(hexstring) hexstring = hexstring.replace('0x', '') - mb = Mutibs.from_hex(hexstring) - return MutableBitStore(mb) + return ConstBitStore(Tibs.from_hex(hexstring)) -def oct2bitstore(octstring: str) -> MutableBitStore: +def oct2bitstore(octstring: str) -> ConstBitStore: octstring = tidy_input_string(octstring) octstring = octstring.replace('0o', '') - mb = Mutibs.from_oct(octstring) - return MutableBitStore(mb) + return ConstBitStore(Tibs.from_oct(octstring)) -def int2bitstore(i: int, length: int, signed: bool) -> MutableBitStore: +def int2bitstore(i: int, length: int, signed: bool) -> ConstBitStore: i = int(i) mb = _int_to_mutibs(i, length, signed, little_endian=False) - return MutableBitStore(mb) + return ConstBitStore(mb.to_tibs()) -def intle2bitstore(i: int, length: int, signed: bool) -> MutableBitStore: +def intle2bitstore(i: int, length: int, signed: bool) -> ConstBitStore: i = int(i) mb = _int_to_mutibs(i, length, signed, little_endian=True) - return MutableBitStore(mb) + return ConstBitStore(mb.to_tibs()) -def float2bitstore(f: Union[str, float], length: int, big_endian: bool) -> MutableBitStore: +def float2bitstore(f: Union[str, float], length: int, big_endian: bool) -> ConstBitStore: f = float(f) - mb = Mutibs.from_f(f, length, Endianness.Big if big_endian else Endianness.Little) - return MutableBitStore(mb) + t = Tibs.from_f(f, length, Endianness.Big if big_endian else Endianness.Little) + return ConstBitStore(t) CACHE_SIZE = 256 @@ -133,22 +130,22 @@ def bitstore_from_token(name: str, token_length: Optional[int], value: Optional[ -def ue2bitstore(i: Union[str, int]) -> MutableBitStore: +def ue2bitstore(i: Union[str, int]) -> ConstBitStore: i = int(i) if i < 0: raise bitstring.CreationError("Cannot use negative initialiser for unsigned exponential-Golomb.") if i == 0: - return MutableBitStore.from_bin('1') + return ConstBitStore.from_bin('1') tmp = i + 1 leadingzeros = -1 while tmp > 0: tmp >>= 1 leadingzeros += 1 remainingpart = i + 1 - (1 << leadingzeros) - return MutableBitStore.from_bin('0' * leadingzeros + '1') + int2bitstore(remainingpart, leadingzeros, False) + return ConstBitStore.from_bin('0' * leadingzeros + '1') + int2bitstore(remainingpart, leadingzeros, False) -def se2bitstore(i: Union[str, int]) -> MutableBitStore: +def se2bitstore(i: Union[str, int]) -> ConstBitStore: i = int(i) if i > 0: u = (i * 2) - 1 @@ -157,22 +154,22 @@ def se2bitstore(i: Union[str, int]) -> MutableBitStore: return ue2bitstore(u) -def uie2bitstore(i: Union[str, int]) -> MutableBitStore: +def uie2bitstore(i: Union[str, int]) -> ConstBitStore: i = int(i) if i < 0: raise bitstring.CreationError("Cannot use negative initialiser for unsigned interleaved exponential-Golomb.") - return MutableBitStore.from_bin('1' if i == 0 else '0' + '0'.join(bin(i + 1)[3:]) + '1') + return ConstBitStore.from_bin('1' if i == 0 else '0' + '0'.join(bin(i + 1)[3:]) + '1') -def sie2bitstore(i: Union[str, int]) -> MutableBitStore: +def sie2bitstore(i: Union[str, int]) -> ConstBitStore: i = int(i) if i == 0: - return MutableBitStore.from_bin('1') + return ConstBitStore.from_bin('1') else: - return uie2bitstore(abs(i)) + (MutableBitStore.from_bin('1') if i < 0 else MutableBitStore.from_bin('0')) + return uie2bitstore(abs(i)) + (ConstBitStore.from_bin('1') if i < 0 else ConstBitStore.from_bin('0')) -def bfloat2bitstore(f: Union[str, float], big_endian: bool) -> MutableBitStore: +def bfloat2bitstore(f: Union[str, float], big_endian: bool) -> ConstBitStore: f = float(f) fmt = '>f' if big_endian else ' MutableBitStore: except OverflowError: # For consistency, we overflow to 'inf'. b = struct.pack(fmt, float('inf') if f > 0 else float('-inf')) - return MutableBitStore.from_bytes(b[0:2]) if big_endian else MutableBitStore.from_bytes(b[2:4]) + return ConstBitStore.from_bytes(b[0:2]) if big_endian else ConstBitStore.from_bytes(b[2:4]) -def p4binary2bitstore(f: Union[str, float]) -> MutableBitStore: +def p4binary2bitstore(f: Union[str, float]) -> ConstBitStore: f = float(f) u = p4binary_fmt.float_to_int8(f) return int2bitstore(u, 8, False) -def p3binary2bitstore(f: Union[str, float]) -> MutableBitStore: +def p3binary2bitstore(f: Union[str, float]) -> ConstBitStore: f = float(f) u = p3binary_fmt.float_to_int8(f) return int2bitstore(u, 8, False) -def e4m3mxfp2bitstore(f: Union[str, float]) -> MutableBitStore: +def e4m3mxfp2bitstore(f: Union[str, float]) -> ConstBitStore: f = float(f) if bitstring.options.mxfp_overflow == 'saturate': u = e4m3mxfp_saturate_fmt.float_to_int(f) @@ -204,7 +201,7 @@ def e4m3mxfp2bitstore(f: Union[str, float]) -> MutableBitStore: return int2bitstore(u, 8, False) -def e5m2mxfp2bitstore(f: Union[str, float]) -> MutableBitStore: +def e5m2mxfp2bitstore(f: Union[str, float]) -> ConstBitStore: f = float(f) if bitstring.options.mxfp_overflow == 'saturate': u = e5m2mxfp_saturate_fmt.float_to_int(f) @@ -213,7 +210,7 @@ def e5m2mxfp2bitstore(f: Union[str, float]) -> MutableBitStore: return int2bitstore(u, 8, False) -def e3m2mxfp2bitstore(f: Union[str, float]) -> MutableBitStore: +def e3m2mxfp2bitstore(f: Union[str, float]) -> ConstBitStore: f = float(f) if math.isnan(f): raise ValueError("Cannot convert float('nan') to e3m2mxfp format as it has no representation for it.") @@ -221,7 +218,7 @@ def e3m2mxfp2bitstore(f: Union[str, float]) -> MutableBitStore: return int2bitstore(u, 6, False) -def e2m3mxfp2bitstore(f: Union[str, float]) -> MutableBitStore: +def e2m3mxfp2bitstore(f: Union[str, float]) -> ConstBitStore: f = float(f) if math.isnan(f): raise ValueError("Cannot convert float('nan') to e2m3mxfp format as it has no representation for it.") @@ -229,7 +226,7 @@ def e2m3mxfp2bitstore(f: Union[str, float]) -> MutableBitStore: return int2bitstore(u, 6, False) -def e2m1mxfp2bitstore(f: Union[str, float]) -> MutableBitStore: +def e2m1mxfp2bitstore(f: Union[str, float]) -> ConstBitStore: f = float(f) if math.isnan(f): raise ValueError("Cannot convert float('nan') to e2m1mxfp format as it has no representation for it.") @@ -240,10 +237,10 @@ def e2m1mxfp2bitstore(f: Union[str, float]) -> MutableBitStore: e8m0mxfp_allowed_values = [float(2 ** x) for x in range(-127, 128)] -def e8m0mxfp2bitstore(f: Union[str, float]) -> MutableBitStore: +def e8m0mxfp2bitstore(f: Union[str, float]) -> ConstBitStore: f = float(f) if math.isnan(f): - return MutableBitStore.from_bin('11111111') + return ConstBitStore.from_bin('11111111') try: i = e8m0mxfp_allowed_values.index(f) except ValueError: @@ -251,15 +248,15 @@ def e8m0mxfp2bitstore(f: Union[str, float]) -> MutableBitStore: return int2bitstore(i, 8, False) -def mxint2bitstore(f: Union[str, float]) -> MutableBitStore: +def mxint2bitstore(f: Union[str, float]) -> ConstBitStore: f = float(f) if math.isnan(f): raise ValueError("Cannot convert float('nan') to mxint format as it has no representation for it.") f *= 2 ** 6 # Remove the implicit scaling factor if f > 127: # 1 + 63/64 - return MutableBitStore.from_bin('01111111') + return ConstBitStore.from_bin('01111111') if f <= -128: # -2 - return MutableBitStore.from_bin('10000000') + return ConstBitStore.from_bin('10000000') # Want to round to nearest, so move by 0.5 away from zero and round down by converting to int if f >= 0.0: f += 0.5 From 46ac2407b20ff9fadfa28d03dd0ab7e1fcab4c9c Mon Sep 17 00:00:00 2001 From: Scott Griffiths Date: Sat, 9 May 2026 18:55:15 +0100 Subject: [PATCH 34/35] More small performance improvements. --- bitstring/bitstore_helpers.py | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/bitstring/bitstore_helpers.py b/bitstring/bitstore_helpers.py index e16d59eb..1b723890 100644 --- a/bitstring/bitstore_helpers.py +++ b/bitstring/bitstore_helpers.py @@ -1,6 +1,6 @@ from __future__ import annotations -from tibs import Tibs, Mutibs, Endianness +from tibs import Tibs, Endianness import struct import math @@ -18,11 +18,11 @@ from bitstring.helpers import tidy_input_string -def _int_to_mutibs(i: int, length: int, signed: bool, little_endian: bool) -> Mutibs: +def _int_to_tibs(i: int, length: int, signed: bool, little_endian: bool) -> Tibs: try: if signed: - return Mutibs.from_i(i, length, Endianness.Little if little_endian else Endianness.Unspecified) - return Mutibs.from_u(i, length, Endianness.Little if little_endian else Endianness.Unspecified) + return Tibs.from_i(i, length, Endianness.Little if little_endian else Endianness.Unspecified) + return Tibs.from_u(i, length, Endianness.Little if little_endian else Endianness.Unspecified) except (OverflowError, ValueError) as e: # Keep tibs validation for normal sizes and unsupported values. if length <= 128: @@ -30,7 +30,7 @@ def _int_to_mutibs(i: int, length: int, signed: bool, little_endian: bool) -> Mu byteorder = "little" if little_endian else "big" b = i.to_bytes((length + 7) // 8, byteorder=byteorder, signed=signed) offset = (-length) % 8 - return Mutibs.from_bytes(b, offset=offset, length=length) + return Tibs.from_bytes(b, offset=offset, length=length) def bin2bitstore(binstring: str) -> ConstBitStore: @@ -53,14 +53,14 @@ def oct2bitstore(octstring: str) -> ConstBitStore: def int2bitstore(i: int, length: int, signed: bool) -> ConstBitStore: i = int(i) - mb = _int_to_mutibs(i, length, signed, little_endian=False) - return ConstBitStore(mb.to_tibs()) + t = _int_to_tibs(i, length, signed, little_endian=False) + return ConstBitStore(t) def intle2bitstore(i: int, length: int, signed: bool) -> ConstBitStore: i = int(i) - mb = _int_to_mutibs(i, length, signed, little_endian=True) - return ConstBitStore(mb.to_tibs()) + t = _int_to_tibs(i, length, signed, little_endian=True) + return ConstBitStore(t) def float2bitstore(f: Union[str, float], length: int, big_endian: bool) -> ConstBitStore: @@ -97,6 +97,11 @@ def _oct_literal_to_const_bitstore(octstring: str) -> ConstBitStore: @functools.lru_cache(CACHE_SIZE) def str_to_bitstore(s: str) -> ConstBitStore: + # Fast path for literal-only strings (e.g. "0xff, 0b101, 0o7"). + try: + return ConstBitStore(Tibs.from_string(s)) + except ValueError: + pass _, tokens = bitstring.utils.tokenparser(s) constbitstores = [bitstore_from_token(*token) for token in tokens] return ConstBitStore.join(constbitstores) From 528517bb8d4a8380bb1c734c2b6ee9401c9c1a62 Mon Sep 17 00:00:00 2001 From: Scott Griffiths Date: Sat, 9 May 2026 19:16:33 +0100 Subject: [PATCH 35/35] Fast path for cut with a const bitstore. --- bitstring/bits.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/bitstring/bits.py b/bitstring/bits.py index 8746c672..d97b777b 100644 --- a/bitstring/bits.py +++ b/bitstring/bits.py @@ -1264,6 +1264,21 @@ def cut(self, bits: int, start: Optional[int] = None, end: Optional[int] = None, raise ValueError("Cannot cut - count must be >= 0.") if bits <= 0: raise ValueError("Cannot cut - bits must be >= 0.") + if isinstance(self._bitstore, ConstBitStore): + source_tibs = self._bitstore.tibs if (start_ == 0 and end_ == len(self)) else self._bitstore.tibs[start_:end_] + cls = self.__class__ + is_stream = isinstance(self, bitstring.ConstBitStream) + emitted = 0 + for chunk_tibs in source_tibs.chunks_iter(bits): + if count is not None and emitted >= count: + return + chunk = object.__new__(cls) + chunk._bitstore = ConstBitStore(chunk_tibs) + if is_stream: + chunk._pos = 0 + emitted += 1 + yield chunk + return c = 0 while count is None or c < count: c += 1