diff --git a/bitformat/array_.py b/bitformat/array_.py index 7fc9859..82e25b2 100644 --- a/bitformat/array_.py +++ b/bitformat/array_.py @@ -4,7 +4,7 @@ import numbers from collections.abc import Sized from bitformat.exceptions import CreationError -from typing import Union, List, Iterable, Any, Optional, overload, TextIO +from typing import Union, Iterable, Any, overload, TextIO from bitformat.bits import Bits, BitsType from bitformat.dtypes import Dtype, dtype_register from bitformat import utils @@ -62,8 +62,8 @@ class Array: """ - def __init__(self, dtype: Union[str, Dtype], initializer: Optional[Union[int, Array, Iterable, Bits, bytes, bytearray, memoryview]] = None, - trailing_bits: Optional[BitsType] = None) -> None: + def __init__(self, dtype: Union[str, Dtype], initializer: Union[int, Array, Iterable, Bits, bytes, bytearray, memoryview] | None = None, + trailing_bits: BitsType | None = None) -> None: self._data = BitStore() try: self._set_dtype(dtype) @@ -230,7 +230,7 @@ def astype(self, dtype: Union[str, Dtype]) -> Array: new_array = self.__class__(dtype, self.tolist()) return new_array - def tolist(self) -> List[ElementType]: + def tolist(self) -> list[ElementType]: return [self._dtype.read_fn(self.data, start=start) for start in range(0, len(self.data) - self._dtype.length + 1, self._dtype.length)] @@ -312,7 +312,7 @@ def reverse(self) -> None: raise ValueError(f"Cannot reverse the items in the Array as its data length ({len(self.data)} bits) is not a multiple of the format length ({self._dtype.length} bits).") self.data = Bits.join([self.data[s - self._dtype.length: s] for s in range(len(self.data), 0, -self._dtype.length)]) - def pp(self, fmt: Optional[str] = None, width: int = 120, + def pp(self, fmt: str | None = None, width: int = 120, show_offset: bool = True, stream: TextIO = sys.stdout) -> None: """Pretty-print the Array contents. diff --git a/bitformat/bits.py b/bitformat/bits.py index 78c7e76..71dcda8 100644 --- a/bitformat/bits.py +++ b/bitformat/bits.py @@ -5,7 +5,7 @@ import struct import io from collections import abc -from typing import Tuple, Union, List, Iterable, Any, Optional, TextIO, overload, Iterator, Type, TypeVar +from typing import Union, Iterable, Any, TextIO, overload, Iterator, Type, TypeVar import bitformat from .bitstore import BitStore from bitformat import bitstore_helpers, utils @@ -13,7 +13,7 @@ from bitformat.bitstring_options import Colour # Things that can be converted to Bits when a Bits type is needed -BitsType = Union['Bits', str, Iterable[Any], bool, bytearray, bytes, memoryview] +BitsType = Union['Bits', str, Iterable[Any], bool, bytearray, bytes, memoryview, io.BytesIO] TBits = TypeVar("TBits", bound='Bits') @@ -62,7 +62,7 @@ def build(cls, dtype: Dtype | str, value: Any, /) -> TBits: return d.build(value) @classmethod - def fromstring(cls: TBits, s: str, /) -> TBits: + def fromstring(cls, s: str, /) -> TBits: """Create a new bitstring from a formatted string.""" x = super().__new__(cls) x._bitstore = bitstore_helpers.str_to_bitstore(s) @@ -104,7 +104,19 @@ def _create_from_bitstype(cls: Type[TBits], auto: BitsType, /) -> TBits: if isinstance(auto, cls): return auto b = super().__new__(cls) - b._setauto_no_length_or_offset(auto) + if isinstance(auto, str): + b._bitstore = bitstore_helpers.str_to_bitstore(auto) + elif isinstance(auto, Bits): + b._bitstore = auto._bitstore.copy() + elif isinstance(auto, (bytes, bytearray, memoryview)): + b._bitstore = BitStore.frombytes(bytearray(auto)) + elif isinstance(auto, io.BytesIO): + b._bitstore = BitStore.frombytes(auto.getvalue()) + elif isinstance(auto, abc.Iterable): + # Evaluate each item as True or False and set bits to 1 or 0. + b._setbin_unsafe(''.join(str(int(bool(x))) for x in auto)) + else: + raise TypeError(f"Cannot initialise Bits from type '{type(auto)}'.") return b def __iter__(self) -> Iterable[bool]: @@ -204,7 +216,7 @@ def __str__(self) -> str: def _repr(self, classname: str, length: int): if length == 0: s = '' - if length % 4 == 0: + elif length % 4 == 0: s = '0x' + self.parse('hex') else: s = '0b' + self.parse('bin') @@ -221,7 +233,7 @@ def __repr__(self) -> str: def __eq__(self, bs: Any, /) -> bool: """Return True if two bitstrings have the same binary representation. - >>> Bits.fromstring('0b1110') == '0xe' + >>> Bits('0b1110') == '0xe' True """ @@ -233,7 +245,7 @@ def __eq__(self, bs: Any, /) -> bool: def __ne__(self, bs: Any, /) -> bool: """Return False if two bitstrings have the same binary representation. - >>> Bits.fromstring('0b111') == '0x7' + >>> Bits('0b111') == '0x7' False """ @@ -339,27 +351,11 @@ def __bool__(self) -> bool: """Return False if bitstring is empty, otherwise return True.""" return len(self) != 0 - def _setauto_no_length_or_offset(self, s: BitsType, /) -> None: - """Set Bits from a Bits, bytes, iterable or string.""" - if isinstance(s, str): - self._bitstore = bitstore_helpers.str_to_bitstore(s) - elif isinstance(s, Bits): - self._bitstore = s._bitstore.copy() - elif isinstance(s, (bytes, bytearray, memoryview)): - self._bitstore = BitStore.frombytes(bytearray(s)) - elif isinstance(s, io.BytesIO): - self._bitstore = BitStore.frombytes(s.getvalue()) - elif isinstance(s, abc.Iterable): - # Evaluate each item as True or False and set bits to 1 or 0. - self._setbin_unsafe(''.join(str(int(bool(x))) for x in s)) - else: - raise TypeError(f"Cannot initialise bitstring from type '{type(s)}'.") - def _setbits(self, bs: BitsType, length: None = None) -> None: bs = Bits._create_from_bitstype(bs) self._bitstore = bs._bitstore - def _setbytes(self, data: Union[bytearray, bytes, List], length: None = None) -> None: + def _setbytes(self, data: Union[bytearray, bytes, list], length: None = None) -> None: """Set the data from a bytes or bytearray object.""" self._bitstore = BitStore.frombytes(bytes(data)) @@ -379,7 +375,7 @@ def _getbytes_printable(self) -> str: string = ''.join(chr(0x100 + x) if x in Bits._unprintable else chr(x) for x in bytes_) return string - def _setuint(self, uint: int, length: Optional[int] = None) -> None: + def _setuint(self, uint: int, length: int | None = None) -> None: """Reset the bitstring to have given unsigned int interpretation.""" # If no length given, and we've previously been given a length, use it. if length is None and hasattr(self, 'len') and len(self) != 0: @@ -394,7 +390,7 @@ def _getuint(self) -> int: raise bitformat.InterpretError("Cannot interpret a zero length bitstring as an integer.") return self._bitstore.slice_to_uint() - def _setint(self, int_: int, length: Optional[int] = None) -> None: + def _setint(self, int_: int, length: int | None = None) -> None: """Reset the bitstring to have given signed int interpretation.""" # If no length given, and we've previously been given a length, use it. if length is None and hasattr(self, 'len') and len(self) != 0: @@ -409,7 +405,7 @@ def _getint(self) -> int: raise bitformat.InterpretError("Cannot interpret bitstring without a length as an integer.") return self._bitstore.slice_to_int() - def _setfloat(self, f: float, length: Optional[int]) -> None: + def _setfloat(self, f: float, length: int | None) -> None: if length is None and hasattr(self, 'len') and len(self) != 0: length = len(self) if length is None or length not in [16, 32, 64]: @@ -496,7 +492,7 @@ def _addright(self, bs: Bits, /) -> None: def _getbits(self: TBits): return self._copy() - def _validate_slice(self, start: Optional[int], end: Optional[int]) -> Tuple[int, int]: + def _validate_slice(self, start: int | None, end: int | None) -> tuple[int, int]: """Validate start and end and return them as positive bit positions.""" start = 0 if start is None else (start + len(self) if start < 0 else start) end = len(self) if end is None else (end + len(self) if end < 0 else end) @@ -504,8 +500,8 @@ def _validate_slice(self, start: Optional[int], end: Optional[int]) -> Tuple[int raise ValueError(f"Invalid slice positions for bitstring length {len(self)}: start={start}, end={end}.") return start, end - def find(self, bs: BitsType, /, start: Optional[int] = None, end: Optional[int] = None, - bytealigned: Optional[bool] = None) -> Union[Tuple[int], Tuple[()]]: + def find(self, bs: BitsType, /, start: int | None = None, end: int | None = None, + bytealigned: bool | None = None) -> Union[tuple[int], tuple[()]]: """Find first occurrence of substring bs. Returns a single item tuple with the bit position if found, or an @@ -534,13 +530,13 @@ def find(self, bs: BitsType, /, start: Optional[int] = None, end: Optional[int] p = self._find(bs, start, end, ba) return p - def _find(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 == -1 else (p,) - def findall(self, bs: BitsType, start: Optional[int] = None, end: Optional[int] = None, count: Optional[int] = None, - bytealigned: Optional[bool] = None) -> Iterable[int]: + def findall(self, bs: BitsType, start: int| None = None, end: int | None = None, count: int | None = None, + bytealigned: bool | None = None) -> Iterable[int]: """Find all occurrences of bs. Return generator of bit positions. bs -- The bitstring to find. @@ -564,7 +560,7 @@ def findall(self, bs: BitsType, start: Optional[int] = None, end: Optional[int] ba = bitformat.options.bytealigned if bytealigned is None else bytealigned return self._findall(bs, start, end, count, ba) - def _findall(self, bs: Bits, start: int, end: int, count: Optional[int], + def _findall(self, bs: Bits, start: int, end: int, count: int | None, bytealigned: bool) -> Iterable[int]: c = 0 for i in self._bitstore.findall(bs._bitstore, start, end, bytealigned): @@ -574,8 +570,8 @@ def _findall(self, bs: Bits, start: int, end: int, count: Optional[int], yield i return - def rfind(self, bs: BitsType, /, start: Optional[int] = None, end: Optional[int] = None, - bytealigned: Optional[bool] = None) -> Union[Tuple[int], Tuple[()]]: + def rfind(self, bs: BitsType, /, start: int | None = None, end: int | None = None, + bytealigned: bool | None = None) -> Union[tuple[int], tuple[()]]: """Find final occurrence of substring bs. Returns a single item tuple with the bit position if found, or an @@ -601,13 +597,13 @@ def rfind(self, bs: BitsType, /, start: Optional[int] = None, end: Optional[int] p = self._rfind(bs, start, end, ba) return p - def _rfind(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 == -1 else (p,) - def cut(self, bits: int, start: Optional[int] = None, end: Optional[int] = None, - count: Optional[int] = None) -> Iterator[Bits]: + def cut(self, bits: int, start: int | None = None, end: int | None = None, + count: int | None = None) -> Iterator[Bits]: """Return bitstring generator by cutting into bits sized chunks. bits -- The size in bits of the bitstring chunks to generate. @@ -643,7 +639,7 @@ def tobytes(self) -> bytes: """ return self._bitstore.tobytes() - def startswith(self, prefix: BitsType, start: Optional[int] = None, end: Optional[int] = None) -> bool: + def startswith(self, prefix: BitsType, start: int | None = None, end: int | None = None) -> bool: """Return whether the current bitstring starts with prefix. prefix -- The bitstring to search for. @@ -655,7 +651,7 @@ def startswith(self, prefix: BitsType, start: Optional[int] = None, end: Optiona start, end = self._validate_slice(start, end) return self._slice(start, start + len(prefix)) == prefix if end >= start + len(prefix) else False - def endswith(self, suffix: BitsType, start: Optional[int] = None, end: Optional[int] = None) -> bool: + def endswith(self, suffix: BitsType, start: int | None = None, end: int | None = None) -> bool: """Return whether the current bitstring ends with suffix. suffix -- The bitstring to search for. @@ -667,7 +663,7 @@ def endswith(self, suffix: BitsType, start: Optional[int] = None, end: Optional[ start, end = self._validate_slice(start, end) return self._slice(end - len(suffix), end) == suffix if start + len(suffix) <= end else False - def all(self, value: Any, pos: Optional[Iterable[int]] = None) -> bool: + def all(self, value: Any, pos: Iterable[int] | None = None) -> bool: """Return True if one or many bits are all set to bool(value). value -- If value is True then checks for bits set to 1, otherwise @@ -684,7 +680,7 @@ def all(self, value: Any, pos: Optional[Iterable[int]] = None) -> bool: return False return True - def any(self, value: Any, pos: Optional[Iterable[int]] = None) -> bool: + def any(self, value: Any, pos: Iterable[int] | None = None) -> bool: """Return True if any of one or many bits are set to bool(value). value -- If value is True then checks for bits set to 1, otherwise @@ -717,7 +713,7 @@ def count(self, value: Any) -> int: @staticmethod def _format_bits(bits: Bits, bits_per_group: int, sep: str, dtype: Dtype, - colour_start: str, colour_end: str, width: Optional[int] = None) -> Tuple[str, int]: + colour_start: str, colour_end: str, width: int | None = None) -> tuple[str, int]: get_fn = dtype.get_fn if dtype.name == 'bytes': # Special case for bytes to print one character each. get_fn = Bits._getbytes_printable @@ -740,7 +736,7 @@ def _format_bits(bits: Bits, bits_per_group: int, sep: str, dtype: Dtype, return x, chars_used @staticmethod - def _chars_per_group(bits_per_group: int, fmt: Optional[str]): + def _chars_per_group(bits_per_group: int, fmt: str | None): """How many characters are needed to represent a number of bits with a given format.""" if fmt is None or dtype_register[fmt].bitlength2chars_fn is None: return 0 @@ -753,7 +749,7 @@ def _bits_per_char(fmt: str): raise ValueError 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, + def _pp(self, dtype1: Dtype, dtype2: Dtype | None, bits_per_group: int, width: int, sep: str, format_sep: str, show_offset: bool, stream: TextIO, offset_factor: int) -> None: """Internal pretty print method.""" colour = Colour(not bitformat.options.no_color) @@ -850,7 +846,7 @@ def _process_pp_tokens(token_list, fmt): bits_per_group //= 2 return dtype1, dtype2, bits_per_group, has_length_in_fmt - def pp(self, fmt: Optional[str] = None, width: int = 120, sep: str = ' ', + def pp(self, fmt: str | None = None, width: int = 120, sep: str = ' ', show_offset: bool = True, stream: TextIO = sys.stdout) -> None: """Pretty print the bitstring's value. diff --git a/bitformat/bitstore.py b/bitformat/bitstore.py index d753dd3..cf99fb0 100644 --- a/bitformat/bitstore.py +++ b/bitformat/bitstore.py @@ -2,7 +2,7 @@ import bitarray import copy -from typing import Union, Iterable, Optional, Iterator, Any +from typing import Union, Iterable, Iterator, Any class BitStore: @@ -44,19 +44,19 @@ def setall(self, value: int, /) -> None: def tobytes(self) -> bytes: return self._bitarray.tobytes() - def slice_to_uint(self, start: Optional[int] = None, end: Optional[int] = None) -> int: + def slice_to_uint(self, start: int | None = None, end: int | None = None) -> int: return bitarray.util.ba2int(self.getslice(start, end)._bitarray, signed=False) - def slice_to_int(self, start: Optional[int] = None, end: Optional[int] = None) -> int: + def slice_to_int(self, start: int | None = None, end: int | None = None) -> int: return bitarray.util.ba2int(self.getslice(start, end)._bitarray, signed=True) - def slice_to_hex(self, start: Optional[int] = None, end: Optional[int] = None) -> str: + def slice_to_hex(self, start: int | None = None, end: int | None = None) -> str: return bitarray.util.ba2hex(self.getslice(start, end)._bitarray) - def slice_to_bin(self, start: Optional[int] = None, end: Optional[int] = None) -> str: + def slice_to_bin(self, start: int | None = None, end: int | None = None) -> str: return self.getslice(start, end)._bitarray.to01() - def slice_to_oct(self, start: Optional[int] = None, end: Optional[int] = None) -> str: + def slice_to_oct(self, start: int | None = None, end: int | None = None) -> str: return bitarray.util.ba2base(8, self.getslice(start, end)._bitarray) def __iadd__(self, other: BitStore, /) -> BitStore: @@ -178,10 +178,10 @@ def getindex(self, index: int, /) -> bool: def getslice_withstep(self, key: slice, /) -> BitStore: return BitStore.frombitarray(self._bitarray.__getitem__(key)) - def getslice(self, start: Optional[int], stop: Optional[int], /) -> BitStore: + def getslice(self, start: int | None, stop: int | None, /) -> BitStore: return BitStore.frombitarray(self._bitarray[start:stop]) - def invert(self, index: Optional[int] = None, /) -> None: + def invert(self, index: int | None = None, /) -> None: if index is not None: self._bitarray.invert(index) else: diff --git a/bitformat/bitstore_helpers.py b/bitformat/bitstore_helpers.py index d017ce5..48621b0 100644 --- a/bitformat/bitstore_helpers.py +++ b/bitformat/bitstore_helpers.py @@ -2,7 +2,7 @@ import struct import functools -from typing import Union, Optional, Dict, Callable +from typing import Union, Dict, Callable import bitarray from bitformat.bitstore import BitStore import bitformat @@ -103,7 +103,7 @@ def float2bitstore(f: Union[str, float], length: int) -> BitStore: } -def bitstore_from_token(name: str, token_length: Optional[int], value: Optional[str]) -> BitStore: +def bitstore_from_token(name: str, token_length: int | None, value: str | None) -> BitStore: if name in literal_bit_funcs: return literal_bit_funcs[name](value) try: diff --git a/bitformat/dtypes.py b/bitformat/dtypes.py index 01bf157..5cddfdf 100644 --- a/bitformat/dtypes.py +++ b/bitformat/dtypes.py @@ -1,7 +1,7 @@ from __future__ import annotations import functools -from typing import Optional, Dict, Any, Union, Tuple, Callable +from typing import Dict, Any, Union, Tuple, Callable import inspect import bitformat from bitformat import utils @@ -27,11 +27,11 @@ class Dtype: _is_signed: bool _set_fn_needs_length: bool _variable_length: bool - _bitlength: Optional[int] + _bitlength: int | None _bits_per_item: int - _length: Optional[int] + _length: int | None - def __new__(cls, token: Union[str, Dtype], /, length: Optional[int] = None) -> Dtype: + def __new__(cls, token: Union[str, Dtype], /, length: int | None = None) -> Dtype: if isinstance(token, cls): return token if length is None: @@ -52,7 +52,7 @@ def length(self) -> int: return self._length @property - def bitlength(self) -> Optional[int]: + def bitlength(self) -> int | None: """The number of bits needed to represent a single instance of the data type. Set to None for variable length dtypes.""" return self._bitlength @@ -77,7 +77,7 @@ def is_signed(self) -> bool: return self._is_signed @property - def set_fn(self) -> Optional[Callable]: + def set_fn(self) -> Union[Callable, None]: """A function to set the value of the data type.""" return self._set_fn @@ -102,7 +102,7 @@ def __hash__(self) -> int: @classmethod @functools.lru_cache(CACHE_SIZE) - def _create(cls, definition: DtypeDefinition, length: Optional[int]) -> Dtype: + def _create(cls, definition: DtypeDefinition, length: int | None) -> Dtype: x = super().__new__(cls) x._name = definition.name x._bitlength = x._length = length @@ -257,7 +257,7 @@ def read_fn(bs, start): self.read_fn = read_fn self.bitlength2chars_fn = bitlength2chars_fn - def get_dtype(self, length: Optional[int] = None) -> Dtype: + def get_dtype(self, length: int | None = None) -> Dtype: if self.allowed_lengths: if length is None: if self.allowed_lengths.only_one_value(): @@ -285,7 +285,7 @@ def __repr__(self) -> str: class Register: """A singleton class that holds all the DtypeDefinitions. Not (yet) part of the public interface.""" - _instance: Optional[Register] = None + _instance: Register | None = None names: Dict[str, DtypeDefinition] = {} def __new__(cls) -> Register: @@ -310,7 +310,7 @@ def add_dtype_alias(cls, name: str, alias: str): property(fget=definition.get_fn, doc=f"An alias for '{name}'. Read only.")) @classmethod - def get_dtype(cls, name: str, length: Optional[int]) -> Dtype: + def get_dtype(cls, name: str, length: int | None) -> Dtype: try: definition = cls.names[name] except KeyError: diff --git a/bitformat/utils.py b/bitformat/utils.py index 30cae06..afe7e3b 100644 --- a/bitformat/utils.py +++ b/bitformat/utils.py @@ -2,8 +2,7 @@ import functools import re -from typing import Tuple, List, Optional, Pattern, Dict, Union, Match - +from typing import Tuple, List, Pattern, Union # A token name followed by an integer number NAME_INT_RE: Pattern[str] = re.compile(r'^([a-zA-Z][a-zA-Z0-9_]*?)(\d*)$') @@ -26,7 +25,7 @@ BYTESWAP_STRUCT_PACK_RE: Pattern[str] = re.compile(r'^(?P[<>@=])?(?P(?:\d*[bBhHlLqQefd])+)$') @functools.lru_cache(CACHE_SIZE) -def parse_name_length_token(fmt: str, **kwargs) -> Tuple[str, Optional[int]]: +def parse_name_length_token(fmt: str, **kwargs) -> Tuple[str, int | None]: # Any single token with just a name and length if m2 := NAME_INT_RE.match(fmt): name = m2.group(1) @@ -47,7 +46,7 @@ def parse_name_length_token(fmt: str, **kwargs) -> Tuple[str, Optional[int]]: @functools.lru_cache(CACHE_SIZE) -def parse_single_token(token: str) -> Tuple[str, str, Optional[str]]: +def parse_single_token(token: str) -> Tuple[str, str, str | None]: if (equals_pos := token.find('=')) == -1: value = None else: @@ -97,7 +96,7 @@ def preprocess_tokens(fmt: str) -> List[str]: @functools.lru_cache(CACHE_SIZE) def tokenparser(fmt: str, keys: Tuple[str, ...] = ()) -> \ - Tuple[bool, List[Tuple[str, Union[int, str, None], Optional[str]]]]: + Tuple[bool, List[Tuple[str, Union[int, str, None], str | None]]]: """Divide the format string into tokens and parse them. Return stretchy token and list of [initialiser, length, value] @@ -112,7 +111,7 @@ def tokenparser(fmt: str, keys: Tuple[str, ...] = ()) -> \ """ tokens = preprocess_tokens(fmt) stretchy_token = False - ret_vals: List[Tuple[str, Union[str, int, None], Optional[str]]] = [] + ret_vals: List[Tuple[str, Union[str, int, None], str | None]] = [] for token in tokens: if keys and token in keys: # Don't bother parsing it, it's a keyword argument