# Copyright 2016 - 2020 Alexey Stepanov aka penguinolog
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""BinField module.
Implements BinField in Python
"""
from __future__ import annotations
import copy
import math
import typing
__all__ = ("BinField",)
KeyT = typing.Union[str, int, slice, typing.Tuple[int, int], typing.List[int]]
IndexT = typing.Union[int, slice, typing.Iterable[int], typing.Dict[str, typing.Tuple[int, int]]]
ResolvedMappingT = typing.Dict[
str,
typing.Union[int, slice, typing.Dict[str, typing.Any]],
]
DeclaredMappingT = typing.Dict[
str,
typing.Union[int, slice, typing.Tuple[int, int], typing.List[int], typing.Dict[str, typing.Any]],
]
AllowedMappingT = typing.Optional[DeclaredMappingT]
# Resolve mapping
# _size_ : int -> _size_ + _mask_
# _mask_ : int -> _mask_ + _size_
# _index_ -> only for nested structures
#
# Values
# slice -> slice
# tuple(int, int) -> slice
# list(int, int) -> slice
# int -> int
def _is_descriptor(obj: typing.Any) -> bool:
"""Return True if obj is a descriptor, False otherwise."""
return hasattr(obj, "__get__") or hasattr(obj, "__set__") or hasattr(obj, "__delete__")
def _is_dunder(name: str) -> bool:
"""Return True if a __dunder__ name, False otherwise."""
return name[:2] == name[-2:] == "__" and name[2:3] != "_" and name[-3:-2] != "_" and len(name) > 4
def _is_sunder(name: str) -> bool:
"""Return True if a _sunder_ name, False otherwise."""
return name[0] == name[-1] == "_" and name[1:2] != "_" and name[-2:-1] != "_" and len(name) > 2
def _is_valid_slice(obj: typing.Union[slice, typing.Any]) -> bool:
"""Slice is valid for BinField operations.
:type obj: slice
:rtype: bool
"""
if not isinstance(obj, slice) or obj.step is not None:
return False
if obj.start is not None and obj.stop is not None:
return 0 <= obj.start < obj.stop # type: ignore
return True
def _is_valid_slice_mapping(obj: typing.Union[typing.List[int], typing.Tuple[int, int], typing.Any]) -> bool:
"""Object is valid slice mapping.
:rtype: bool
"""
return (
isinstance(obj, (tuple, list))
and len(obj) == 2
and isinstance(obj[0], int)
and isinstance(obj[1], int)
and 0 <= obj[0] < obj[1]
)
def _mapping_filter(key: str, val: typing.Any) -> bool:
"""Filter for naming records from namespace.
:param key: namespace key
:type key: str
:param val: namespace value
:type val: typing.Any
:rtype: bool
"""
if not isinstance(key, str):
return False
if key in {"_index_"}:
return True
# Descriptors, special methods, protected
if _is_descriptor(val) or _is_dunder(key) or key.startswith("_"):
return False
# Index / slice / slice from iterable
if isinstance(val, int) or _is_valid_slice(val) or _is_valid_slice_mapping(val):
return True
# Not nested
if not isinstance(val, dict):
return False
# Process nested
return all((_mapping_filter(k, v) for k, v in val.items()))
def _get_index(val: IndexT) -> typing.Union[int, slice]:
"""Extract real index from index."""
if isinstance(val, int) or _is_valid_slice(val):
return val # type: ignore
if _is_valid_slice_mapping(val):
return slice(*val) # type: ignore
if isinstance(val, dict):
return slice(*val["_index_"])
raise TypeError(f"Unexpected val: {val!r}") # pragma: no cover
def _get_mask(start: int, end: int) -> int:
"""Make default mask.
:type start: int
:type end: int
:rtype: int
"""
return (1 << end) - (1 << start)
def _get_start_index(src: typing.Tuple[typing.Any, IndexT]) -> int:
"""Internal method for sorting mapping.
:param src: tuple from dict.items()
:type src: tuple
:rtype: int
"""
if isinstance(src[1], int):
return src[1]
return _get_index(src[1]).start # type: ignore
def _prepare_mapping(mapping: DeclaredMappingT) -> ResolvedMappingT:
"""Check indexes for intersections.
:type mapping: typing.Dict
:rtype: typing.Dict
:raises ValueError: Unexpected data
:raises IndexError: Mapping after non-ending slice index or mapping intersection
"""
mapping_mask: int = 0
new_mapping: ResolvedMappingT = {}
cycle_end = False
# pylint: disable=undefined-loop-variable
def check_update_mapping_mask(mask: int) -> int:
"""Check mask for validity and return updated value.
:type mask: int
:rtype: int
:raises IndexError: Keys intersection
"""
if mapping_mask & mask:
raise IndexError(f"Mapping key {m_key} has intersection with other keys by mask {mapping_mask & mask:b}")
return mapping_mask | mask
# pylint: enable=undefined-loop-variable
if "_index_" in mapping:
new_mapping["_index_"] = mapping.pop("_index_") # type: ignore
unexpected = [item for item in mapping.items() if not _mapping_filter(*item)]
if unexpected:
raise ValueError(f"Mapping contains unexpected data: {unexpected!r}")
for m_key, m_val in sorted(mapping.items(), key=_get_start_index):
if cycle_end:
raise IndexError(f"Mapping after non-ending slice index! First key: {m_key}")
if isinstance(m_val, (list, tuple)):
new_mapping[m_key] = slice(*m_val) # Mapped slice -> slice
mapping_mask = check_update_mapping_mask(_get_mask(*m_val))
elif isinstance(m_val, int):
mapping_mask = check_update_mapping_mask(_get_mask(m_val, m_val + 1))
new_mapping[m_key] = m_val
elif isinstance(m_val, dict): # nested mapping
mapping_mask = check_update_mapping_mask(_get_mask(*m_val["_index_"]))
new_mapping[m_key] = _prepare_mapping(m_val)
else:
if m_val.stop:
mapping_mask = check_update_mapping_mask(_get_mask(m_val.start if m_val.start else 0, m_val.stop))
else:
if mapping_mask & (1 << m_val.start):
raise IndexError(
f"Mapping key {m_key} has intersection with other keys "
f"by mask {mapping_mask & (1 << m_val.start):b}"
)
cycle_end = True
new_mapping[m_key] = m_val
return new_mapping
def _make_mapping_property(key: str) -> property:
"""Property generator. Fixing lazy calculation.
:rtype: property
"""
def fget(self: typing.MutableMapping[str, typing.Any]) -> typing.Any:
"""Mapping key: {key}."""
return self[key]
def fset(self: typing.MutableMapping[str, typing.Any], val: typing.Any) -> None:
"""Setter for {key}."""
self[key] = val
return property(fget=fget, fset=fset, doc=f"mapping key: {key}")
def _make_static_ro_property(name: str, val: typing.Any) -> property:
"""Property generator for static cases.
:type name: str
:type val: object
"""
return property(fget=lambda _: val, doc=f"Read-only {name}")
class BaseBinFieldMeta: # pragma: no cover
"""Fake class for BinFieldMeta compilation and class instance creation."""
__slots__ = ()
class BinField(typing.MutableMapping[str, typing.Any]): # pragma: no cover
"""Fake class for BinFieldMeta compilation & MyPy help."""
_size_: typing.Optional[int]
_mask_: typing.Optional[int]
_mapping_: AllowedMappingT
_value_: int
_bit_size_: int
class BaseMeta(type): # pragma: no cover
"""Metaclass for BaseClass creation."""
@property
def _value_(cls) -> typing.Any:
"""Internal value (integer)."""
return NotImplemented
@property
def _size_(cls) -> typing.Any:
"""Only for sized (Not BaseClass)."""
return NotImplemented
@property
def _bit_size_(cls) -> typing.Any:
"""Only for sized (Not BaseClass)."""
return NotImplemented
@property
def _mask_(cls) -> typing.Any:
"""Only if mask presents (Not BaseClass)."""
return NotImplemented
@property
def _mapping_(cls) -> typing.Any:
"""Only for indexed (Not BaseClass)."""
return NotImplemented
class BinFieldMeta(BaseMeta):
"""Metaclass for BinField class and subclasses construction."""
# noinspection PyInitNewSignature
def __new__( # type: ignore
mcs, # noqa:N804
name: str,
bases: typing.Tuple[type],
classdict: typing.Dict[str, typing.Any],
) -> typing.Type[BinField]:
"""Metaclass for BinField.
:type name: str
:type bases: tuple
:type classdict: dict
:returns: new class
:raises ValueError: validation fail (size, mask, reserved keys in classdict)
:raises TypeError: Invalid type for size or mask, or unexpected data in classdict
"""
if not (BaseBinFieldMeta in bases or any((issubclass(base, BaseBinFieldMeta) for base in bases))):
# Top level baseclass: cleanup
for key in "_value_", "_size_", "_mask_", "_mapping_": # pragma: no cover
classdict.pop(key, None)
return super().__new__(mcs, name, bases, classdict)
meta_dict = {}
meta_name = f"{name}Meta"
if "_index_" in classdict:
raise ValueError("_index_ is reserved index for slicing nested BinFields")
size = classdict.pop("_size_", None)
mask_from_size = None
if size is not None:
if not isinstance(size, int):
raise TypeError(f"Pre-defined size has invalid type: {size!r}")
if size <= 0:
raise ValueError("Size must be positive value !")
mask_from_size = (1 << size) - 1
mask = classdict.pop("_mask_", mask_from_size)
if mask is not None:
if not isinstance(mask, int):
raise TypeError(f"Pre-defined mask has invalid type: {mask!r}")
if mask < 0:
raise ValueError("BitMask is strictly positive!")
if size is None:
# noinspection PyUnresolvedReferences
size = mask.bit_length()
meta_dict["_size_"] = classdict["_size_"] = _make_static_ro_property("size", size)
meta_dict["_mask_"] = classdict["_mask_"] = _make_static_ro_property("mask", mask)
mapping = classdict.pop("_mapping_", None)
if mapping is None:
mapping = {}
for m_key, m_val in classdict.copy().items():
if not _mapping_filter(m_key, m_val):
continue
if isinstance(m_val, (list, tuple)):
mapping[m_key] = slice(*m_val) # Mapped slice -> slice
else:
mapping[m_key] = m_val
del classdict[m_key]
garbage = {
name: obj
for name, obj in classdict.items()
if not (_is_dunder(name) or _is_sunder(name) or _is_descriptor(obj))
}
if garbage:
raise TypeError(f"Several data is not recognized in class structure: {garbage!r}")
ready_mapping = _prepare_mapping(mapping)
if ready_mapping:
meta_dict["_mapping_"] = classdict["_mapping_"] = _make_static_ro_property(
"mapping", copy.deepcopy(ready_mapping)
)
for m_key in ready_mapping:
classdict[m_key] = _make_mapping_property(m_key)
meta_dict[m_key] = _make_static_ro_property(m_key, _get_index(ready_mapping[m_key]))
else:
meta_dict["_mapping_"] = classdict["_mapping_"] = _make_static_ro_property("mapping", None)
classdict["_cache_"] = {} # Use for subclasses memorize
if BinField not in bases:
return super().__new__(mcs, name, bases, classdict)
# noinspection PyPep8Naming
RealMeta = type(meta_name, (type,), meta_dict) # noqa:N806 # pylint: disable=invalid-name # NOSONAR
# pylint: disable=bad-mcs-classmethod-argument
class SubMeta(RealMeta, BinFieldMeta): # type: ignore
"""Mixin metaclass for creating BinField subclasses.
Properties is made in RealMeta and here we are creating new class
by the single possible way (usage of super() is impossible).
:raises TypeError: Subclassing of BinField
"""
# noinspection PyMethodParameters,PyInitNewSignature
def __new__(
smcs, # noqa:N804 # NOSONAR
sname: str,
sbases: typing.Tuple[type],
sns: typing.Dict[str, typing.Any],
) -> SubMeta: # pylint: disable=undefined-variable
for base in sbases:
if base is not BinField and issubclass(base, BinField):
raise TypeError("Cannot extend BinField")
sns["__slots__"] = () # No any new fields on instances
return super().__new__(smcs, sname, sbases, sns) # type: ignore
# pylint: enable=bad-mcs-classmethod-argument
return type.__new__(SubMeta, name, bases, classdict)
@classmethod
def makecls(
mcs, # noqa:N804
name: str,
mapping: AllowedMappingT = None,
mask: typing.Optional[int] = None,
size: typing.Optional[int] = None,
) -> typing.Type[BinField]:
"""Create new BinField subclass.
:param name: Class name
:type name: str
:param mapping: Data mapping
:type mapping: dict
:param mask: Data mask for new class
:type mask: int
:param size: BinField bit length
:type size: int
:returns: BinField subclass
"""
classdict: typing.Dict[str, typing.Any] = {"_size_": size, "_mask_": mask, "__slots__": ()}
if mapping is not None:
classdict["_mapping_"] = mapping
# noinspection PyTypeChecker
return mcs.__new__(mcs, name, (BinField,), classdict)
# noinspection PyRedeclaration
BaseBinFieldMeta = type.__new__(BinFieldMeta, "BaseBinFieldMeta", (), {"__slots__": ()}) # type: ignore # noqa: F811
# noinspection PyRedeclaration,PyMissingConstructor
[docs]class BinField(BaseBinFieldMeta): # type: ignore # noqa: F811 # pylint: disable=function-redefined
"""BinField representation."""
__slots__ = ["__value", "__parent_link"]
# Will be replaced by the same by metaclass, but helps lint
_cache_: typing.Dict[typing.Tuple[int, str], BinField] = {}
_size_: typing.Optional[int] = None
_mask_: typing.Optional[int] = None
_mapping_: AllowedMappingT = None
def __init__(
self,
x: typing.Union[int, str] = 0, # type
base: int = 10,
_parent: typing.Optional[typing.Tuple[BinField, int]] = None,
) -> None:
"""Create new BinField object from integer value.
:param x: Start value
:type x: typing.Union[int, str, bytes]
:param base: base for start value
:type base: int
:param _parent: Parent link. For internal usage only.
:type _parent: typing.Optional[typing.Tuple[BinField, int]]
"""
self.__value: int = x if isinstance(x, int) else int(x, base=base)
if self._mask_:
self.__value &= self._mask_
self.__parent_link = _parent
@property
def _bit_size_(self) -> int:
"""Number of bits necessary to represent self in binary.
Could be frozen by constructor
:rtype: int
"""
return self._size_ if self._size_ else self._value_.bit_length()
def __len__(self) -> int:
"""Data length in bytes."""
length = int(math.ceil(self._bit_size_ / 8.0))
return length if length else 1
@property
def _value_(self) -> int:
"""Internal value (integer).
:rtype: int
"""
if self.__parent_link is not None: # Update value from parent
obj, offset = self.__parent_link
self.__value = (obj & (self._mask_ << offset)) >> offset # type: ignore
return self.__value
# noinspection PyProtectedMember
@_value_.setter
def _value_(self, new_value: int) -> None:
"""Internal value (integer).
:type new_value: int
"""
if self._mask_:
new_value &= self._mask_
if self.__parent_link is not None:
obj, offset = self.__parent_link
obj[:] = int(obj) & ~(self._mask_ << offset) | (new_value << offset) # type: ignore
self.__value = new_value
# integer methods
[docs] def __int__(self) -> int:
"""Conversion to normal int.
:rtype: int
"""
return self._value_
[docs] def __index__(self) -> int:
"""Special method used for bin()/hex/oct/slicing support.
:rtype: int
"""
return self._value_
# math operators
[docs] def __abs__(self) -> int:
"""Mimic int.
:rtype: int
"""
return self._value_
[docs] def __gt__(self, other: typing.Any) -> bool:
"""Comparing logic.
:rtype: bool
"""
return self._value_ > int(other)
[docs] def __ge__(self, other: typing.Any) -> bool:
"""Comparing logic.
:rtype: bool
"""
return self._value_ >= int(other)
[docs] def __lt__(self, other: typing.Any) -> bool:
"""Comparing logic.
:rtype: bool
"""
return self._value_ < int(other)
[docs] def __le__(self, other: typing.Any) -> bool:
"""Comparing logic.
:rtype: bool
"""
return self._value_ <= int(other)
[docs] def __eq__(self, other: typing.Any) -> bool:
"""Comparing logic.
:rtype: bool
"""
# As integer
if isinstance(other, (int, self.__class__)):
return self._value_ == other
if isinstance(other, BinField):
# noinspection PyUnresolvedReferences,PyProtectedMember
return self._value_ == other._value_ and self._mapping_ == other._mapping_ and len(self) == len(other)
return False
[docs] def __ne__(self, other: typing.Any) -> bool:
"""Comparing logic.
:rtype: bool
"""
return not self == other
# Modify Bitwise operations
[docs] def __iand__(self, other: typing.Any) -> BinField:
"""Mimic int."""
self._value_ &= int(other)
return self
[docs] def __ior__(self, other: typing.Any) -> BinField:
"""Mimic int."""
self._value_ |= int(other)
return self
[docs] def __ixor__(self, other: typing.Any) -> BinField:
"""Mimic int."""
self._value_ ^= int(other)
return self
# Non modify operations: new BinField will re-use _mapping_
[docs] def __and__(self, other: typing.Any) -> BinField:
"""Mimic int.
:rtype: BinField
"""
return self.__class__(self._value_ & int(other))
[docs] def __rand__(self, other: typing.Any) -> typing.Any:
"""Reverse and."""
return other & self._value_
[docs] def __or__(self, other: typing.Any) -> BinField:
"""Mimic int.
:rtype: BinField
"""
return self.__class__(self._value_ | int(other))
[docs] def __ror__(self, other: typing.Any) -> typing.Any:
"""Reverse or."""
return other | self._value_
[docs] def __xor__(self, other: typing.Any) -> BinField:
"""Mimic int.
:rtype: BinField
"""
return self.__class__(self._value_ ^ int(other))
[docs] def __rxor__(self, other: typing.Any) -> typing.Any:
"""Reverse xor."""
return other ^ self._value_
# Integer modify operations
[docs] def __iadd__(self, other: typing.Any) -> BinField:
"""Mimic int.
:raises OverflowError: Result not fills in data length
:raises ValueError: negative result
"""
res = self._value_ + int(other)
if self._size_ and self._size_ < res.bit_length():
raise OverflowError(f"Result value {res} not fill in data length ({self._size_} bits)")
if res < 0:
raise ValueError("BinField could not be negative!")
self._value_ = res
return self
[docs] def __isub__(self, other: typing.Any) -> BinField:
"""Mimic int."""
return self.__iadd__(-other)
# Integer non-modify operations. New object is BinField, if not overflow
# new BinField will re-use _mapping_
[docs] def __add__(self, other: typing.Any) -> typing.Union[int, BinField]:
"""Mimic int.
:rtype: typing.Union[int, BinField]
:raises ValueError: negative result
"""
res = self._value_ + int(other)
if res < 0:
raise ValueError(f"BinField could not be negative! Value {other} is bigger, than {self._value_}")
if self._size_ and self._size_ < res.bit_length():
return res
return self.__class__(res)
[docs] def __radd__(self, other: typing.Any) -> typing.Any:
"""Reverse add."""
return other + self._value_
[docs] def __sub__(self, other: typing.Any) -> typing.Union[int, BinField]:
"""Mimic int.
:rtype: typing.Union[int, BinField]
"""
return self.__add__(-other)
[docs] def __rsub__(self, other: typing.Any) -> typing.Any:
"""Reverse sub."""
return other - self._value_
# Integer -> integer operations
[docs] def __mul__(self, other: typing.Any) -> int:
"""Mimic int.
:rtype: int
"""
return self._value_ * other # type: ignore
[docs] def __rmul__(self, other: typing.Any) -> typing.Any:
"""Reverse multiply."""
return other * self._value_
[docs] def __lshift__(self, other: typing.Any) -> int:
"""Mimic int.
:rtype: int
"""
return self._value_ << other # type: ignore
[docs] def __rlshift__(self, other: typing.Any) -> typing.Any:
"""Reverse left shift."""
return other << self._value_
[docs] def __rshift__(self, other: typing.Any) -> int:
"""Mimic int.
:rtype: int
"""
return self._value_ >> other # type: ignore
[docs] def __rrshift__(self, other: typing.Any) -> typing.Any:
"""Reverse right shift."""
return other >> self._value_
[docs] def __bool__(self) -> bool:
"""Mimic int.
:rtype: bool
"""
return bool(self._value_) # pragma: no cover
# Data manipulation: hash, pickle
[docs] def __hash__(self) -> int:
"""Usage for indexes."""
return hash(
(
self.__class__,
self._value_,
# link is not included, but linked objects will have different
# base classes due to on the fly generation
)
)
[docs] def __copy__(self) -> BinField:
"""Copy logic.
:rtype: BinField
.. note:: Uplink is destroyed on copy.
"""
return self.__class__(self._value_)
[docs] def __getstate__(self) -> typing.Dict[str, int]:
"""Pickling.
:rtype: typing.Dict[str: int]
:raises ValueError: Pickle of linked instance
"""
if self.__parent_link:
raise ValueError("Linked BinFields does not supports pickle")
return {"x": self.__value}
[docs] def __setstate__(self, state: typing.Dict[str, int]) -> None:
"""Restore from pickle.
:type state: typing.Dict[str: int]
"""
self.__init__(**state) # type: ignore # getstate returns enough data for __init__
@classmethod
def _get_child_cls_(
cls,
mask: int,
name: str,
cls_mask: int,
size: int,
mapping: AllowedMappingT = None,
) -> typing.Type[BinField]:
"""Get child class with memorize support.
:type mask: int
:type name: str
:type cls_mask: int
:type size: int
:type mapping: typing.Optional[typing.Dict[str, typing.Union[slice, int, typing.Dict]]]
"""
# Memorize
if (mask, name) not in cls._cache_:
new_cls = BinFieldMeta.makecls(name=name, mapping=mapping, mask=cls_mask, size=size)
cls._cache_[(mask, name)] = new_cls # type: ignore
new_cls = cls._cache_[(mask, name)] # type: ignore
return new_cls
# Access as dict
def _getslice_(
self,
item: slice,
mapping: AllowedMappingT = None,
name: typing.Optional[str] = None,
) -> BinField:
"""Get slice from self.
:type item: slice
:type mapping: typing.Optional[typing.Dict]
:type name: typing.Optional[str]
:rtype: BinField
:raises IndexError: Index out of data length
"""
if item.start is None and item.stop is None:
return self.__copy__()
if item.start and self._size_ and item.start > self._size_:
raise IndexError(f"Index {item} is out of data length {self._size_}")
if name is None:
name = f"{self.__class__.__name__}_slice_{item.start if item.start else 0!s}_{item.stop!s}"
stop = item.stop if item.stop and (not self._size_ or item.stop < self._size_) else self._bit_size_
start = item.start if item.start else 0
mask = _get_mask(start, stop)
if self._mask_ is not None:
mask &= self._mask_
cls_mask = mask >> start
# Memorize
cls = self._get_child_cls_(mask=mask, name=name, cls_mask=cls_mask, size=stop - start, mapping=mapping)
return cls((self._value_ & mask) >> start, _parent=(self, start)) # type: ignore
[docs] def __getitem__(self, item: KeyT) -> BinField:
"""Extract bits.
:type item: typing.Union[str, int, slice, typing.Tuple[int, int], typing.List[int, int]]
:rtype: BinField
:raises IndexError: Mapping is not available
"""
if isinstance(item, int):
name = f"{self.__class__.__name__}_index_{item}"
return self._getslice_(slice(item, item + 1), name=name)
if _is_valid_slice(item):
return self._getslice_(item) # type: ignore
if _is_valid_slice_mapping(item):
return self._getslice_(slice(*item)) # type: ignore
if not isinstance(item, str) or item.startswith("_"):
raise IndexError(item)
if self._mapping_ is None:
raise IndexError("Mapping is not available")
idx = self._mapping_.get(item)
if isinstance(idx, int):
return self._getslice_(slice(idx, idx + 1), name=item)
if isinstance(idx, slice):
return self._getslice_(idx, name=item)
if isinstance(idx, dict): # Nested _mapping_
# Extract slice
slc = slice(*idx["_index_"])
# Build new _mapping_ dict
mapping: ResolvedMappingT = copy.deepcopy(idx)
del mapping["_index_"]
# Get new val
return self._getslice_(slc, mapping=mapping, name=item) # type: ignore
raise IndexError(item)
def _setslice_(self, key: slice, value: int) -> None:
"""Set value by slice.
:type key: slice
:type value: int
:raises OverflowError: Data value to set is bigger, than BinField size or stop is out of length
:raises ValueError: Data bigger, than slice
"""
# Copy scenario
if key.start is None and key.stop is None:
if self._size_ and value.bit_length() > self._size_:
raise OverflowError(
f"Data value to set is bigger, than BinField size: {value.bit_length()} > {self._size_}"
)
self._value_ = value
return
if self._size_ and key.stop and key.stop > self._size_:
raise OverflowError(f"Stop index is out of data length: {key.stop} > {self._size_}")
stop = key.stop if key.stop else self._bit_size_
start = key.start if key.start else 0
if value.bit_length() > stop:
raise ValueError("Data size is bigger, than slice")
if key.start and value.bit_length() > stop - start:
raise ValueError("Data size is bigger, than slice")
value <<= start # Get correct binary position
get_mask = _get_mask(start, stop)
if self._mask_:
get_mask &= self._mask_
self._value_ = self._value_ & ~get_mask | value
[docs] def __setitem__(self, key: KeyT, value: int) -> None:
"""Indexed setter.
:type key: typing.Union[str, int, slice, typing.Tuple[int, int], typing.List[int, int]]
:type value: int
:raises TypeError: value type is not int
:raises IndexError: key not found (or key is not string, no mapping)
"""
if not isinstance(value, int):
raise TypeError("BinField value could be set only as int")
if isinstance(key, int):
return self._setslice_(slice(key, key + 1), value)
if _is_valid_slice(key):
return self._setslice_(key, value) # type: ignore
if _is_valid_slice_mapping(key):
return self._setslice_(slice(*key), value) # type: ignore
if not isinstance(key, str):
raise IndexError(key)
if self._mapping_ is None:
raise IndexError("Mapping is not available")
idx = self._mapping_.get(key)
if isinstance(idx, (int, slice)):
return self.__setitem__(idx, value)
if isinstance(idx, dict) and _is_valid_slice_mapping(idx["_index_"]): # Nested _mapping_
# Extract slice from nested
return self._setslice_(slice(*idx["_index_"]), value)
raise IndexError(key)
# Representations
def __pretty_str__(self, parser: typing.Any, indent: int, no_indent_start: bool) -> str:
"""Real __str__ code."""
indent = 0 if no_indent_start else indent
indent_step = 2 if parser is None else parser.indent_step
max_indent = 20 if parser is None else parser.max_indent
formatter = _Formatter(max_indent=max_indent, indent_step=indent_step)
return formatter(src=self, indent=indent)
def __str__(self) -> str:
"""Public __str__ for usage in print."""
# noinspection PyTypeChecker
return self.__pretty_str__(None, 0, True)
def __pretty_repr__(self, _: typing.Any, indent: int, no_indent_start: bool) -> str:
"""Real __repr__ code."""
indent = 0 if no_indent_start else indent
if self.__parent_link:
pre = "<"
post = f" at 0x{id(self):X}>"
else:
pre = post = ""
return f"{'':<{indent}}{pre}{self.__class__.__name__}(x=0x{self._value_:0{len(self) * 2}X}, base=16){post}"
def __repr__(self) -> str:
"""Public __repr__ for logging/debugging usage."""
return self.__pretty_repr__(None, 0, True)
def __dir__(self) -> typing.List[str]:
"""__dir__ wrapper (used as completion-helper)."""
if self._mapping_ is not None:
keys = list(sorted(self._mapping_.keys()))
else:
keys = []
return ["_bit_size_", "_mapping_", "_mask_", "_value_", "_size_"] + keys
class _Formatter:
def __init__(self, max_indent: int = 20, indent_step: int = 4) -> None:
"""Dedicated str formatter for BinField.
:param max_indent: maximal indent before classic repr() call
:type max_indent: int
:param indent_step: step for the next indentation level
:type indent_step: int
"""
self.__max_indent = max_indent
self.__indent_step = indent_step
@property
def indent_step(self) -> int:
"""Indent step getter.
:rtype: int
"""
return self.__indent_step
def next_indent(self, indent: int, multiplier: int = 1) -> int:
"""Next indentation value.
:param indent: current indentation value
:type indent: int
:param multiplier: steps amount
:type multiplier: int
:rtype: int
"""
return indent + multiplier * self.indent_step
@property
def max_indent(self) -> int:
"""Max indent getter.
:rtype: int
"""
return self.__max_indent
def _str_bf_items(self, src: typing.Dict[str, BinField], indent: int = 0) -> typing.Iterator[str]:
"""Wrapper for repr dict items.
:param src: object to process
:type src: dict
:param indent: start indentation
:type indent: int
:rtype: typing.Iterator[str]
"""
max_len = max([len(str(key)) for key in src]) if src else 0
next_indent = self.next_indent(indent)
for key, val in src.items():
repr_val = self.process_element(val, indent=self.next_indent(indent, multiplier=2), no_indent_start=True)
yield f"\n{'':<{next_indent}}{key!s:{max_len}} = {repr_val}"
# noinspection PyUnresolvedReferences,PyProtectedMember
def process_element(self, src: BinField, indent: int = 0, no_indent_start: bool = False) -> str:
"""Make human readable representation of object.
:param src: object to process
:type src: BinField
:param indent: start indentation
:type indent: int
:param no_indent_start: do not indent open bracket and simple parameters
:type no_indent_start: bool
:return: formatted string
:rtype: str
"""
# pylint: disable=protected-access
if src._mask_ is None:
mask = ""
else:
mask = f" & 0b{src._mask_:b}"
value: int = src._value_
as_hex = f"0x{value:0{len(src) * 2}X}"
as_bin = f"0b{value:0{src._bit_size_}b}{mask}"
if src._mapping_ and indent < self.max_indent:
as_dict = {key: src[key] for key in src._mapping_}
result = "".join(self._str_bf_items(src=as_dict, indent=indent))
newline = "\n" if no_indent_start else ""
return f"{newline}{'':<{indent}}<{value} == {as_hex} == ({as_bin}){result}\n{'':<{indent}}>"
indent = 0 if no_indent_start else indent
return f"{'':<{indent}}<{value} == {as_hex} == ({as_bin})>"
def __call__(self, src: BinField, indent: int = 0, no_indent_start: bool = False) -> str:
"""Make human readable representation of object.
:param src: object to process
:type src: BinField
:param indent: start indentation
:type indent: int
:param no_indent_start: do not indent open bracket and simple parameters
:type no_indent_start: bool
:return: formatted string
"""
result = self.process_element(src, indent=indent, no_indent_start=no_indent_start)
return result