Commit 3df43fd7 authored by Dan Gass's avatar Dan Gass
Browse files

Alpha 14.

parent b3685332
......@@ -33,7 +33,7 @@ more complete usage examples. The following summarizes the
:doc:`plum.bytearray </tutorials/types/bytearray>` array of bytes
:doc:`plum.float </tutorials/types/float>` floating point
:doc:`plum.int </tutorials/types/int>` integers
:doc:`plum.int.bitfields </tutorials/types/int_bitfields>` integer with bitfield accessors
:doc:`plum.int.bitfields </tutorials/types/int_bitfields>` integer with bit field accessors
:doc:`plum.int.enum </tutorials/types/int_enum>` integer enumerated constants
:doc:`plum.int.flag </tutorials/types/int_flag>` integer with bit flags
:doc:`plum.nil </tutorials/types/nil>` no bytes
......
......@@ -190,18 +190,18 @@ provide mechanisms to pack or unpack sequences of bytes and conveniently
specify or access data within the bytes. But |BitFields| types provide
access to individual bits and/or bit fields within the bytes buffer.
The example below demonstrates use of the :func:`bitfield` function
The example below demonstrates use of the :class:`BitField` class
to define the bit position and bit field size (in bits) for each
member in the bytes buffer. Notice, access to the bitfield members
produces an instance of the Python type from the member definition:
member in the bytes buffer. Notice, bit field access produces an
instance of the Python type from the member definition:
>>> from plum.int.bitfields import BitFields, bitfield
>>> from plum.int.bitfields import BitFields, BitField
>>>
>>> class MyBf(BitFields, nbytes=1):
... bflag: bool = bitfield(pos=0, size=1)
... iflag: int = bitfield(pos=1, size=1)
... nibble: int = bitfield(pos=2, size=4)
... reserved: int = bitfield(pos=6, size=2, default=0)
... bflag: bool = BitField(pos=0, size=1)
... iflag: int = BitField(pos=1, size=1)
... nibble: int = BitField(pos=2, size=4)
... reserved: int = BitField(pos=6, size=2, default=0)
...
>>> # construct instance with keyword arguments,
>>> # fields with default may be omitted
......
......@@ -18,7 +18,7 @@ Plum Types
:mod:`plum.bytearray` array of bytes
:mod:`plum.float` floating point
:mod:`plum.int` integer
:mod:`plum.int.bitfields` integer with bitfield accessors
:mod:`plum.int.bitfields` integer with bit field accessors
:mod:`plum.int.enum` integer enumerated constants
:mod:`plum.int.flag` integer with bit flags
:mod:`plum.nil` no bytes
......
......@@ -8,6 +8,12 @@ Versions increment per `semver <http://semver.org/>`_ (except for alpha series).
Alpha (0.1.0aX) Versions (only serial number X increments)
**********************************************************
+ 0.1.0a14 (2020-Feb-10)
- Allow :class:`BitFields` subclasses to be further subclassed.
- Eliminate ``bitfield`` function and replace with direct use of :class:`BitField`.
- Accept ``cls`` argument in :class:`BitField` to facilitate type annotation being
different than bit field type.
+ 0.1.0a13 (2020-Feb-9)
- Allow structure subclasses to be further subclassed.
- Require use of :class:`Member()` for all structure member definitions.
......@@ -24,8 +30,8 @@ Alpha (0.1.0aX) Versions (only serial number X increments)
+ 0.1.0a12 (2020-Feb-04)
- Fix memory leaks in plum-boost.
- Fix bitfield representations when embedded within a structure or another
bitfield (previously, they lacked the class name and just showed fields).
- Fix bit field representations when embedded within a structure or another
bit field (previously, they lacked the class name and just showed fields).
- Allow __pack__ method to be overridden in structure definitions
(previously metaclass constructed one any that were supplied in class
definition).
......
......@@ -28,16 +28,30 @@ name, followed by a colon and a type (any int-like type), then an equal
sign and finally the position and number of bits of the bit field.
For example:
>>> from plum.int.bitfields import BitFields, bitfield
>>> from plum.int.bitfields import BitFields, BitField
>>>
>>> class Sample(BitFields, nbytes=1, byteorder='little'):
... boolean_flag: bool = bitfield(pos=0, size=1)
... integer_flag: int = bitfield(pos=1, size=1)
... nibble: int = bitfield(pos=2, size=4)
... reserved: int = bitfield(pos=6, size=2)
... boolean_flag: bool = BitField(pos=0, size=1)
... integer_flag: int = BitField(pos=1, size=1)
... nibble: int = BitField(pos=2, size=4)
... reserved: int = BitField(pos=6, size=2)
...
>>>
.. Note::
The bit field definition accepts a ``cls`` argument to specify the type.
This facilitates skipping the type annotation or providing a different
type annotation. For example:
>>> class Sample(BitFields, nbytes=1, byteorder='little'):
... boolean_flag = BitField(cls=bool, pos=0, size=1)
... integer_flag = BitField(cls=int, pos=1, size=1)
... nibble = BitField(cls=int, pos=2, size=4)
... reserved = BitField(cls=int, pos=6, size=2)
...
>>>
***************
Unpacking Bytes
......@@ -207,18 +221,18 @@ Defaulting a Bit Field
To specify a default value to be utilized if a bit field value is not provided
during class instantiation (or packing), use the ``default`` argument of the
:func:`bitfield` definition. For example:
:class:`BitField` definition. For example:
.. code-block:: python
:emphasize-lines: 7
>>> from plum.int.bitfields import BitFields, bitfield
>>> from plum.int.bitfields import BitFields, BitField
>>>
>>> class Sample(BitFields, nbytes=1, byteorder='little'):
... boolean_flag: bool = bitfield(pos=0, size=1)
... integer_flag: int = bitfield(pos=1, size=1)
... nibble: int = bitfield(pos=2, size=4)
... reserved: int = bitfield(pos=6, size=2, default=3)
... boolean_flag: bool = BitField(pos=0, size=1)
... integer_flag: int = BitField(pos=1, size=1)
... nibble: int = BitField(pos=2, size=4)
... reserved: int = BitField(pos=6, size=2, default=3)
...
>>> # 'reserved' bit field not specified
>>> Sample(boolean_flag=False, integer_flag=0, nibble=0)
......@@ -235,9 +249,9 @@ following example shows the result of a default value of ``0xc0`` (bits 6 and 7
:emphasize-lines: 1
>>> class Sample(BitFields, nbytes=1, byteorder='little', default=0xc0):
... boolean_flag: bool = bitfield(pos=0, size=1)
... integer_flag: int = bitfield(pos=1, size=1)
... nibble: int = bitfield(pos=2, size=4)
... boolean_flag: bool = BitField(pos=0, size=1)
... integer_flag: int = BitField(pos=1, size=1)
... nibble: int = BitField(pos=2, size=4)
...
>>> Sample(boolean_flag=False, integer_flag=0, nibble=0).pack()
bytearray(b'\xc0')
......@@ -248,18 +262,18 @@ Ignoring a Bit Field
********************
To specify a bit field to be ignored during comparisons, set the ``ignore`` argument to
``True`` in the :func:`bitfield` definition. For example:
``True`` in the :class:`BitField` definition. For example:
.. code-block:: python
:emphasize-lines: 7
>>> from plum.int.bitfields import BitFields, bitfield
>>> from plum.int.bitfields import BitFields, BitField
>>>
>>> class Sample(BitFields, nbytes=1, byteorder='little'):
... boolean_flag: bool = bitfield(pos=0, size=1)
... integer_flag: int = bitfield(pos=1, size=1)
... nibble: int = bitfield(pos=2, size=4)
... reserved: int = bitfield(pos=6, size=2, ignore=True)
... boolean_flag: bool = BitField(pos=0, size=1)
... integer_flag: int = BitField(pos=1, size=1)
... nibble: int = BitField(pos=2, size=4)
... reserved: int = BitField(pos=6, size=2, ignore=True)
...
>>> sample1 = Sample(boolean_flag=False, integer_flag=0, nibble=0, reserved=0)
>>> sample2 = Sample(boolean_flag=False, integer_flag=0, nibble=0, reserved=3)
......@@ -279,9 +293,9 @@ bit mask value of ``0xc0``:
:emphasize-lines: 1
>>> class Sample(BitFields, nbytes=1, byteorder='little', ignore=0xc0):
... boolean_flag: bool = bitfield(pos=0, size=1)
... integer_flag: int = bitfield(pos=1, size=1)
... nibble: int = bitfield(pos=2, size=4)
... boolean_flag: bool = BitField(pos=0, size=1)
... integer_flag: int = BitField(pos=1, size=1)
... nibble: int = BitField(pos=2, size=4)
...
>>> sample1 = Sample(0x00)
>>> sample2 = Sample(0x80)
......@@ -301,7 +315,7 @@ conversion. The most popular choices besides :class:`int`, are
booleans and enumerations as demonstrated in the following example:
>>> from enum import IntFlag
>>> from plum.int.bitfields import BitFields, bitfield
>>> from plum.int.bitfields import BitFields, BitField
>>>
>>> class Letter(IntFlag): # use IntFlag to tolerate undefined values
...
......@@ -314,8 +328,8 @@ booleans and enumerations as demonstrated in the following example:
...
... """Sample BitFields subclass."""
...
... letter: Letter = bitfield(pos=0, size=4)
... boolean: bool = bitfield(pos=4, size=1)
... letter: Letter = BitField(pos=0, size=4)
... boolean: bool = BitField(pos=4, size=1)
...
>>> sample = Sample()
>>> sample.boolean
......@@ -338,14 +352,14 @@ Bit field definitions also support nested |plum| bit fields types as
demonstrated next:
>>> class Inner(BitFields, nbytes=1):
... a: int = bitfield(pos=0, size=2)
... b: int = bitfield(pos=2, size=2)
... a: int = BitField(pos=0, size=2)
... b: int = BitField(pos=2, size=2)
...
>>>
>>> class Outer(BitFields, nbytes=1):
... h: int = bitfield(pos=0, size=2)
... i: Inner = bitfield(pos=2, size=4)
... j: int = bitfield(pos=6, size=2)
... h: int = BitField(pos=0, size=2)
... i: Inner = BitField(pos=2, size=4)
... j: int = BitField(pos=6, size=2)
...
>>> sample = Outer(h=0, i={'a': 1, 'b': 2}, j=3)
>>> sample.i
......@@ -380,9 +394,9 @@ the previous bit field. For example:
>>> class AutoMix(BitFields, nbytes=1):
...
... f1: int = bitfield(size=2)
... f2: int = bitfield(size=4)
... f3: int = bitfield(size=1, pos=7) # explicit position
... f1: int = BitField(size=2)
... f2: int = BitField(size=4)
... f3: int = BitField(size=1, pos=7) # explicit position
...
>>> AutoMix(0x85).dump()
+--------+--------+-------+-------+---------+
......
......@@ -3,6 +3,6 @@
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
"""Interpret bytes as an unsigned integer with bit fields."""
from ._bitfield import bitfield
from ._bitfield import BitField
from ._bitfields import BitFields
from ._bitfieldstype import BitFieldsType
......@@ -4,17 +4,26 @@
"""Bit field definition."""
from enum import IntEnum
from importlib import import_module
class BitField:
"""Bit field definition."""
"""Bit field definition.
:param int size: size in bits
:param int pos: bit offset of least significant bit
:param int default: initial value when unspecified
:param bool signed: interpret as signed integer
:param bool ignore: do not include field in comparisons
"""
# pylint: disable=too-many-instance-attributes
__slots__ = [
'_make_type',
'_type',
'_cls',
'mask',
'signbit',
'default',
......@@ -24,10 +33,12 @@ class BitField:
'size',
]
def __init__(self, pos, size, default, signed, ignore):
def __init__(self, *, size, cls=None, pos=None, default=None, signed=False, ignore=False):
if pos is not None:
pos = int(pos)
assert pos >= 0
if pos < 0:
raise TypeError(
'bit field position must be greater than or equal to zero')
size = int(size)
signed = bool(signed)
......@@ -35,53 +46,70 @@ class BitField:
if signed:
if size < 2:
raise ValueError(
'signed bitfield must have bit size of 2 or greater')
'signed bit field must have bit size of 2 or greater')
signbit = 1 << (size - 1)
else:
if size < 1:
raise ValueError(
'unsigned bitfield must have bit size of 1 or greater')
'unsigned bit field must have bit size of 1 or greater')
signbit = 0
self.mask = (1 << size) - 1
self.signbit = signbit
self.default = default
self.ignore = ignore
self.name = None # assigned via __set_name__ protocol
self.mask = (1 << size) - 1
self.name = None # filled in later by BitFieldsType
self.pos = pos
self.signbit = signbit
self.size = size
# assigned during BitFields class construction (by
# BitFieldsType.__new__)
self._type = None
# assigned during BitFields class construction (by
# BitFieldsType.__new__)
self._make_type = None
if cls is None:
# filled in later using type annotation byte BitFieldsType
self._cls = None
self._make_type = None
else:
self.cls = cls
@property
def type(self):
def cls(self):
"""Bit field type.
:returns: bit field type
:rtype: type
"""
return self._type
return self._cls
@type.setter
def type(self, cls):
self._type = cls
@cls.setter
def cls(self, cls):
# delay import to avoid circular import issue
bitfields = import_module('.bitfields', 'plum.int')
if issubclass(cls, IntEnum):
self._make_type = self._make_enum
elif issubclass(cls, int):
self._make_type = self._make_int
else:
self._make_type = self._make_bits
self._cls = cls
acceptable = True
try:
if issubclass(cls, IntEnum):
self._make_type = self._make_enum
elif issubclass(cls, int):
self._make_type = self._make_int
elif issubclass(cls, bitfields.BitFields):
self._make_type = self._make_bits
else:
acceptable = False
except TypeError:
acceptable = False # not a class
if not acceptable:
if self.name:
raise TypeError(
f'bit field {self.name!r} type must be int-like')
raise TypeError(
f"bit field type must be int-like")
@property
def signed(self):
"""Indication if bitfield is a signed integer.
"""Indication if bit field is a signed integer.
:returns: indication
:rtype: bool
......@@ -92,17 +120,13 @@ class BitField:
def __repr__(self):
return ('BitField('
f'name={self.name!r},'
f'type={self._type!r},'
f'cls={self._cls!r},'
f'pos={self.pos!r},'
f'size={self.size!r},'
f'default={self.default!r},'
f'signed={self.signed!r}'
')')
def __set_name__(self, owner, name):
assert self.name is None, 'bitfield definition already in use'
self.name = name
def __get__(self, obj, objtype=None):
if obj is None:
return self
......@@ -116,7 +140,7 @@ class BitField:
return self._make_type(obj, value)
def _make_bits(self, obj, value):
item = self._type(value)
item = self._cls(value)
try:
bitoffset, store = obj.__bitoffset_store__
except AttributeError:
......@@ -128,13 +152,13 @@ class BitField:
def _make_int(self, obj, value):
# pylint: disable=unused-argument
return self._type(value)
return self._cls(value)
def _make_enum(self, obj, value):
# pylint: disable=unused-argument
try:
# pylint: disable=not-callable
value = self.type(value)
value = self.cls(value)
except ValueError:
# must not be a part of the enumeration
pass
......@@ -149,8 +173,8 @@ class BitField:
try:
value = int(value)
except TypeError:
# must be a nested bitfield (where value could be a dict)
value = self._type(value)
# must be a nested bit field (where value could be a dict)
value = self._cls(value)
if self.signbit:
minvalue = -(1 << (size - 1))
......@@ -161,7 +185,7 @@ class BitField:
if (value < minvalue) or (value > maxvalue):
raise ValueError(
f'bitfield {self.name!r} requires {minvalue} <= number <= {maxvalue}')
f'bit field {self.name!r} requires {minvalue} <= number <= {maxvalue}')
try:
bitoffset, obj = obj.__bitoffset_store__
......@@ -171,21 +195,3 @@ class BitField:
pos += bitoffset
obj.__value__ = (obj.__value__ & ~(mask << pos)) | ((value & mask) << pos)
# FUTURE: look at dataclass implementation as to why this should be a function
# rather than just leaving user instantiate class directly
# (has something to do with IDE introspection/hints)
def bitfield(*, size, pos=None, default=None, signed=False, ignore=False):
"""Create bit field definition.
:param int size: size in bits
:param int pos: bit offset of least significant bit
:param int default: initial value when unspecified
:param bool signed: interpret as signed integer
:param bool ignore: do not include field in comparisons
:returns: bit field definition
:rtype: BitField
"""
return BitField(pos, size, default, signed, ignore)
......@@ -132,16 +132,16 @@ class BitFields(Plum, metaclass=BitFieldsType, nbytes=4, byteorder='little', def
if not isinstance(value, cls):
value = cls(value)
for name, field in cls.__fields__.items():
if issubclass(field.type, BitFields):
row = dump.add_record(access='.' + name, cls=field.type)
field.type.__add_bitfields_to_dump__(
if issubclass(field.cls, BitFields):
row = dump.add_record(access='.' + name, cls=field.cls)
field.cls.__add_bitfields_to_dump__(
getattr(cls, name).__get__(value, cls), row, bitoffset + field.pos)
else:
dump.add_record(
access='.' + name,
bits=(bitoffset + field.pos, field.size),
value=str(getattr(cls, name).__get__(value, cls)),
cls=field.type)
cls=field.cls)
def __repr__(self):
# str( ) around getattr formats enumerations correctly (otherwise shows
......
......@@ -3,8 +3,6 @@
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
"""BitFields type metaclass."""
from enum import IntEnum
from ..._plumtype import PlumType
from ._bitfield import BitField
......@@ -33,10 +31,10 @@ class BitFieldsType(PlumType):
For example:
>>> from plum.int.bitfields import BitFields, bitfield
>>> from plum.int.bitfields import BitFields, BitField
>>> class MyBits(BitFields, nbytes=1, byteorder='big', default=0, ignore=0x80):
... nibble: int = bitfield(pos=0, size=4)
... threebits: int = bitfield(pos=4, size=3)
... nibble: int = BitField(pos=0, size=4)
... threebits: int = BitField(pos=4, size=3)
...
>>>
......@@ -55,20 +53,30 @@ class BitFieldsType(PlumType):
# validate bit field class attributes
fields = {}
for fieldname, typ in getattr(cls, '__annotations__', {}).items():
field = getattr(cls, fieldname, None)
annotations = getattr(cls, '__annotations__', {})
if not isinstance(field, BitField):
raise TypeError(
f'bit field {fieldname!r} must be assigned a bitfield()')
fields = {}
for fieldname, bitfield in namespace.items():
if not isinstance(bitfield, BitField):
continue
if not (isinstance(typ, BitFieldsType) or issubclass(typ, (int, IntEnum))):
if bitfield.name is None:
bitfield.name = fieldname
else:
raise TypeError(
f'bit field {fieldname!r} type must be int-like')
f"invalid bit field {fieldname!r} definition, "
f"{type(bitfield).__name__}() instance can not be shared "
f"between bit fields classes")
if bitfield.cls is None:
try:
bitfield.cls = annotations[fieldname]
except KeyError:
raise TypeError(
f"bit field {fieldname!r} must be assigned a type using either "
f"the BitField() 'cls' argument or a type annotation")
field.type = typ
fields[fieldname] = field
fields[fieldname] = bitfield
# fill in missing positions
......@@ -145,8 +153,8 @@ class BitFieldsType(PlumType):
for field in fields.values():
if field.ignore:
ignore |= field.mask << field.pos
elif isinstance(field.type, BitFieldsType):
ignore |= field.type.__ignore__ << field.pos
elif isinstance(field.cls, BitFieldsType):
ignore |= field.cls.__ignore__ << field.pos
cls.__byteorder__ = byteorder
cls.__compare_mask__ = (max_ ^ ignore) & max_
......
......@@ -36,7 +36,7 @@ class VersionInfo:
major = 0
minor = 1
micro = 0
serial = 1
serial = 14
version = f'{major}.{minor}.{micro}'
......@@ -60,7 +60,7 @@ __version__ = VersionInfo.version
__author__ = 'Dan Gass'
__maintainer__ = 'Dan Gass'
__email__ = 'dan.gass@gmail.com'
__copyright__ = 'Copyright 2019 Daniel Mark Gass'
__copyright__ = 'Copyright 2020 Daniel Mark Gass'
__license__ = 'MIT License; http://opensource.org/licenses/MIT'
......
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Copyright 2019 Daniel Mark Gass, see __about__.py for license information.
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
"""Test basic features of bitfield member class."""
"""Test basic features of BitField member class."""
import pytest
from baseline import Baseline
from plum.int.bitfields import bitfield
from plum.int.bitfields import BitField
from ...utils import wrap_message
......@@ -18,15 +18,61 @@ class TestFeatures:
def test_repr(self):
"""Verify repr() behavior."""
expected = Baseline("""
BitField(name=None,type=None,pos=0,size=1,default=None,signed=False)
BitField(name=None,cls=None,pos=0,size=1,default=None,signed=False)
""")
assert repr(bitfield(pos=0, size=1)) == expected
assert repr(BitField(pos=0, size=1)) == expected
def test_signed(self):
"""Verify signed property behavior."""
assert bitfield(pos=0, size=2, signed=True).signed