Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions benchmark/test_comparison_example.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,19 +13,22 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
# pyright: basic
import sys
from pathlib import Path

import pytest

import caterpillar
from caterpillar.context import O_CONTEXT_FACTORY

try:
from examples.comparison import comparison_1_caterpillar as caterpillar_default

pytestmark = pytest.mark.benchmark
sys.path.insert(0, str(Path(__file__).parent.parent))

from examples.comparison import comparison_1_caterpillar as caterpillar_default
except ImportError:
caterpillar_default = None

pytestmark = pytest.mark.benchmark

NATIVE_ONLY = pytest.mark.skipif(
not caterpillar.native_support(), reason="native extension unavailable"
Expand Down
2 changes: 2 additions & 0 deletions docs/sphinx/source/library/byteorder.rst
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ Standard Byteorder Instances

.. autodata:: caterpillar.byteorder.Dynamic

.. autodata:: caterpillar.byteorder.Inherit

.. autodata:: caterpillar.byteorder.LITTLE_ENDIAN_FMT

.. autodata:: caterpillar.byteorder.O_DEFAULT_ENDIAN
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,19 @@ Byteorder Extensions
the byteorder applied. Note the use of another operator here.

>>> field = BigEndian + struct

.. versionadded:: 2.10.0

Classes created via :func:`~caterpillar.model.struct` or :func:`~caterpillar.model.bitfield`
also implement :meth:`~object.__set_byteorder__` (installed as a
``classmethod``), so the very same operator syntax works directly on
a struct *class* rather than only on field/atom instances:

>>> @struct(order=Inherit)
... class Inner:
... value: uint32
...
>>> field = LittleEndian + Inner # Field(Inner, order=LittleEndian)

See :ref:`the byte order tutorial <tutorial-dyn_byteorder>` for more details.

Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@

@bitfield(order=LittleEndian)
class Flags:
bool1 : 1
num4 : 3
bool1: 1
num4: 3
# padding is generated automatically


Expand Down
215 changes: 212 additions & 3 deletions docs/sphinx/source/tutorial/advanced/byteorder.rst
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,18 @@ based on the current pack or unpack context.
.. versionadded:: 2.6.4
This feature is available in starting from version ``2.6.4``

.. versionadded:: 2.10.0
Added the ``Inherit`` byteorder type.

There are various use-cases that require a struct to handle both big-endian and
little-endian. In order to reduce the amount of code for these structs, *caterpillar*
introduces a special byte order type: :class:`~caterpillar.byteorder.DynByteOrder`. It
supports two different configuration levels:

1. struct-wide: endianess is configured when calling :func:`~caterpillar.model.pack` or
1. global: endianess is configured when calling :func:`~caterpillar.model.pack` or
:func:`~caterpillar.model.unpack`.
2. field-level: byte order is applied per field
2. struct-level: byte order is applied per struct (handed down to all fields)
3. field-level: byte order is applied per field

Each of these configuration levels support various methods of selecting the target endian:

Expand Down Expand Up @@ -229,4 +232,210 @@ As an example, consider the following definition:

# 1 -> True, results in LittleEndian
obj.spec = 1
data_le = pack(obj)
data_le = pack(obj)


Example: byte order selected by an ancestor
--------------------------------------------

A nested model can select its byte order from an enclosing model by chaining
the :data:`~caterpillar.context.parent` context path. In this example,
``Inner`` is nested through ``Middle``, so
``parent.parent.byte_order`` resolves ``Outer.byte_order``:

.. code-block:: python
:linenos:

@struct(order=Dynamic(parent.parent.byte_order))
class Inner:
value: uint32

@struct
class Middle:
inner: Inner

@struct
class Outer:
byte_order: uint8 # 0 selects BigEndian; 1 selects LittleEndian
middle: Middle

When unpacking, declare ``byte_order`` before ``middle`` so its value is
available when ``Inner`` resolves its dynamic byte order.

Inheriting the enclosing struct's byte order
---------------------------------------------

.. versionadded:: 2.10.0

A struct that is reused in multiple places (e.g. a shared header or record
type) sometimes needs to be decoded using whatever byte order the *embedding*
struct declares, rather than a byte order fixed at its own definition site.
The global :data:`~caterpillar.byteorder.Inherit` enables exactly this:

.. tab-set::
:sync-group: syntax

.. tab-item:: Default Syntax
:sync: default

.. code-block:: python
:linenos:

@struct(order=Inherit)
class Inner:
value: uint32

@struct(order=BigEndian)
class Outer:
inner: Inner # value decoded using BigEndian

@struct(order=LittleEndian)
class Format:
inner: Inner # value decoded using LittleEndian

.. tab-item:: Extended Syntax (>=2.8.0)
:sync: extended

.. code-block:: python
:linenos:

@struct(order=Inherit)
class Inner:
value: uint32_t

@struct(order=BigEndian)
class Outer:
inner: Inner # value decoded using BigEndian

@struct(order=LittleEndian)
class Format:
inner: Inner # value decoded using LittleEndian

A struct declared with ``order=Inherit`` resolves its byte order from the
field that embeds it, at any nesting depth (including arrays and chains of
``Inherit`` structs). If there is no enclosing struct - e.g. the struct is
used standalone or passed directly to :func:`~caterpillar.model.pack` /
:func:`~caterpillar.model.unpack` - it falls back to the regular default
byte order, exactly like a struct declared with ``order=None``.

Wrappers can use an independently ordered metadata field without changing the
order inherited by their payload. For example, this frame has a little-endian
length prefix but a big-endian inner value:

.. code-block:: python
:linenos:

@struct(order=BigEndian)
class Outer:
inner: Prefixed(LittleEndian + uint16, Inner)

``inner.value`` is decoded using ``BigEndian``; the explicit
``LittleEndian`` applies only to the prefix.

To override the byte order for a single embedding site instead of changing
``Inner`` itself, combine ``order=Inherit`` with the ``ByteOrder + Struct``
operator, mirroring the existing per-field ``ByteOrder + <atom>`` syntax:

.. code-block:: python
:linenos:

@struct
class Format:
inner: LittleEndian + Inner # value decoded using LittleEndian

This also lets sibling fields referencing the same ``Inherit``-enabled struct
type resolve to different byte orders independently of one another - including
a plain (non-overridden) reference that keeps inheriting from the enclosing
struct alongside two explicitly-pinned siblings:

.. code-block:: python
:linenos:

@struct(order=BigEndian)
class Format:
little: LittleEndian + Inner # always little
big: BigEndian + Inner # always big
inherited: Inner # follows Format's BigEndian

Changing ``Format``'s own order (e.g. to ``LittleEndian``) only affects the
``inherited`` field; ``little`` and ``big`` stay pinned to their explicit
override since they no longer carry ``Inherit`` at that embedding site.

As with :data:`~caterpillar.byteorder.Dynamic`, this can be composed with the
extended annotation syntax, e.g.
``f[Inner, Inner, LittleEndian]``.

.. note::
This feature is unrelated to the struct *class* inheritance described in
:ref:`datamodel_standard_struct` (subclassing a ``@struct``-decorated class via
normal Python MRO). ``order=Inherit`` is about how a struct picks its byte
order when it is *embedded as a field* in another struct, regardless of
whether either struct participates in Python class inheritance at all.

Nesting depth and mixed byte orders
------------------------------------

``order=Inherit`` resolves through arbitrarily deep nesting, and struct chains
may freely mix explicit byte orders with ``Inherit`` at different levels. Each
``Inherit`` struct resolves against the **nearest enclosing struct that has a
concrete (non-``Inherit``) byte order** - not necessarily its immediate parent
and not necessarily the outermost struct:

.. code-block:: python
:linenos:

@struct(order=Inherit)
class Inner:
value: uint32

@struct(order=LittleEndian) # concrete order - breaks the chain here
class Middle:
inner: Inner

@struct(order=BigEndian)
class Outer:
middle: Middle

# Inner.value is decoded using Middle's LittleEndian, not Outer's
# BigEndian, because Middle already provides
# a concrete order for anything it embeds.

This also means a chain of several ``Inherit`` structs in a row correctly
propagates the *first* concrete order found further up the chain, however deep:

.. code-block:: python
:linenos:

@struct(order=Inherit)
class Inner:
value: uint32

@struct(order=Inherit)
class Middle:
inner: Inner

@struct(order=Inherit)
class Outer:
middle: Middle

@struct(order=BigEndian)
class Format:
outer: Outer

# Format.outer.middle.inner.value is decoded using BigEndian, propagated
# through two intermediate Inherit levels.

The same resolution rule applies uniformly regardless of *how* a struct is
nested, which has been verified for:

* **Arrays**, including arrays nested inside other ``Inherit`` structs
(``items: Inner[2]``, or an array of a struct that itself contains an
array of ``Inherit`` structs).
* :func:`~caterpillar.model.bitfield`, which supports ``order=Inherit`` the
same way regular structs do, whether used as a scalar member, an array
member, or nested multiple levels deep.
* :class:`~caterpillar.py.Prefixed`, since it delegates directly to the
wrapped struct using the same context rather than introducing its own
field wrapper.
* :ref:`tutorial-union` structs, both as a container holding an
``Inherit``-enabled member and as the ``Inherit``-enabled struct itself.
11 changes: 6 additions & 5 deletions examples/bitfield_example.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,20 @@
int8,
unpack,
pack,
Invisible
Invisible,
)
from caterpillar.shortcuts import f
from caterpillar.types import balign_t, int1_t, int3_t


@bitfield
class Format:
b1: int1_t # inferred uint8 type with a width of one bit
_: balign_t = Invisible() # start new uint8 with 7 unused bits
b1: int1_t # inferred uint8 type with a width of one bit
_: balign_t = Invisible() # start new uint8 with 7 unused bits
b2: f[str, (2, CharFactory)] # wraps parsed int to char (string)
b3: f[int, 3 - int8] = 1 # default value is applied -> REVISIT: necessary?
_1: int3_t # unnamed padding to the rest of the byte
b3: f[int, 3 - int8] = 1 # default value is applied -> REVISIT: necessary?
_1: int3_t # unnamed padding to the rest of the byte


s = getstruct(Format)
assert isinstance(s, Bitfield)
Expand Down
2 changes: 1 addition & 1 deletion examples/c_extension/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@
# show any additional implementation of packing or unpacking
# data.
obj = ExampleCAtom(5)
print(obj) # <ExampleCAtom 5>
print(obj) # <ExampleCAtom 5>
4 changes: 3 additions & 1 deletion examples/formats/caf.py
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ class CAFChannelLayout:


@struct(order=BigEndian)
class CAFChunk():
class CAFChunk:
chunk_header: CAFChunkHeader
# this is another simple switch-case structure, but this time we define a
# default option. NOTE: not all chunk types are implemented
Expand All @@ -174,8 +174,10 @@ class CAFChunk():
},
]


CAFChunk_t = f[list[CAFChunk], getstruct(CAFChunk)[...]]


@struct(order=BigEndian)
class CAF:
# interestingly, this file format does not declare how many chunks are
Expand Down
8 changes: 4 additions & 4 deletions examples/formats/nibarchive.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
LittleEndian,
ctx,
pack_file,
f
f,
)

try:
Expand Down Expand Up @@ -161,9 +161,9 @@ class NIBArchive:

# NOTE: we can reference parsed values using 'this.foo' but methods or properties
# can't be used.
class_names: NIBClassName[
this.header.class_name_count
] @ this.header.offset_class_names
class_names: (
NIBClassName[this.header.class_name_count] @ this.header.offset_class_names
)


# print(NIBArchive.__struct__.fields)
Expand Down
1 change: 1 addition & 0 deletions examples/greedy_example.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
# reusable types can be defined using the f[...] shortcut
magic_t = f[bytes, BMP_MAGIC]


@struct(order=BigEndian, kw_only=True)
class Format(struct_factory.mixin):
magic: magic_t = BMP_MAGIC
Expand Down
2 changes: 1 addition & 1 deletion examples/inline_syntax.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,4 @@
# Instead of using the special operator, all default struct classes provide
# wrapper functions for packing and unpacking:
value = uint8.from_bytes(data)
assert uint8.to_bytes(value) == data[:1]
assert uint8.to_bytes(value) == data[:1]
2 changes: 1 addition & 1 deletion examples/mixin_example.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,4 @@ class Format(struct_factory.mixin):
# won't complain here:
values = [Format(i, i) for i in range(2)]
buffer = pack(values, Format[...])
assert unpack(Format[...], buffer) == values
assert unpack(Format[...], buffer) == values
Loading
Loading