diff --git a/benchmark/test_comparison_example.py b/benchmark/test_comparison_example.py
index c540cfb9..6de3345d 100644
--- a/benchmark/test_comparison_example.py
+++ b/benchmark/test_comparison_example.py
@@ -13,19 +13,22 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
# 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"
diff --git a/docs/sphinx/source/library/byteorder.rst b/docs/sphinx/source/library/byteorder.rst
index a4c39e23..dd4f9144 100644
--- a/docs/sphinx/source/library/byteorder.rst
+++ b/docs/sphinx/source/library/byteorder.rst
@@ -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
diff --git a/docs/sphinx/source/reference/datamodel/protocols/byteorder_extensions.rst b/docs/sphinx/source/reference/datamodel/protocols/byteorder_extensions.rst
index 9d14c695..d5be6aee 100644
--- a/docs/sphinx/source/reference/datamodel/protocols/byteorder_extensions.rst
+++ b/docs/sphinx/source/reference/datamodel/protocols/byteorder_extensions.rst
@@ -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 ` for more details.
+
diff --git a/docs/sphinx/source/reference/snippets/comparison_1_caterpillar.py b/docs/sphinx/source/reference/snippets/comparison_1_caterpillar.py
index 597b494e..998aac7c 100644
--- a/docs/sphinx/source/reference/snippets/comparison_1_caterpillar.py
+++ b/docs/sphinx/source/reference/snippets/comparison_1_caterpillar.py
@@ -4,8 +4,8 @@
@bitfield(order=LittleEndian)
class Flags:
- bool1 : 1
- num4 : 3
+ bool1: 1
+ num4: 3
# padding is generated automatically
diff --git a/docs/sphinx/source/tutorial/advanced/byteorder.rst b/docs/sphinx/source/tutorial/advanced/byteorder.rst
index 6e3c240a..a94868d2 100644
--- a/docs/sphinx/source/tutorial/advanced/byteorder.rst
+++ b/docs/sphinx/source/tutorial/advanced/byteorder.rst
@@ -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:
@@ -229,4 +232,210 @@ As an example, consider the following definition:
# 1 -> True, results in LittleEndian
obj.spec = 1
- data_le = pack(obj)
\ No newline at end of file
+ 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 + `` 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.
diff --git a/examples/bitfield_example.py b/examples/bitfield_example.py
index 84b06f13..297bb4e9 100644
--- a/examples/bitfield_example.py
+++ b/examples/bitfield_example.py
@@ -6,7 +6,7 @@
int8,
unpack,
pack,
- Invisible
+ Invisible,
)
from caterpillar.shortcuts import f
from caterpillar.types import balign_t, int1_t, int3_t
@@ -14,11 +14,12 @@
@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)
diff --git a/examples/c_extension/main.py b/examples/c_extension/main.py
index e32660c2..f1d3f23a 100644
--- a/examples/c_extension/main.py
+++ b/examples/c_extension/main.py
@@ -5,4 +5,4 @@
# show any additional implementation of packing or unpacking
# data.
obj = ExampleCAtom(5)
-print(obj) #
\ No newline at end of file
+print(obj) #
diff --git a/examples/formats/caf.py b/examples/formats/caf.py
index 554ad461..9956123d 100644
--- a/examples/formats/caf.py
+++ b/examples/formats/caf.py
@@ -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
@@ -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
diff --git a/examples/formats/nibarchive.py b/examples/formats/nibarchive.py
index 92c68022..c7200398 100644
--- a/examples/formats/nibarchive.py
+++ b/examples/formats/nibarchive.py
@@ -28,7 +28,7 @@
LittleEndian,
ctx,
pack_file,
- f
+ f,
)
try:
@@ -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)
diff --git a/examples/greedy_example.py b/examples/greedy_example.py
index 49392ec6..440c1d95 100644
--- a/examples/greedy_example.py
+++ b/examples/greedy_example.py
@@ -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
diff --git a/examples/inline_syntax.py b/examples/inline_syntax.py
index 5ef9bc00..8f0d8b7b 100644
--- a/examples/inline_syntax.py
+++ b/examples/inline_syntax.py
@@ -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]
\ No newline at end of file
+assert uint8.to_bytes(value) == data[:1]
diff --git a/examples/mixin_example.py b/examples/mixin_example.py
index b15d1af1..54b975a2 100644
--- a/examples/mixin_example.py
+++ b/examples/mixin_example.py
@@ -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
\ No newline at end of file
+assert unpack(Format[...], buffer) == values
diff --git a/examples/operator_example.py b/examples/operator_example.py
index ea512ca3..5d94b48a 100644
--- a/examples/operator_example.py
+++ b/examples/operator_example.py
@@ -3,18 +3,19 @@
# Here, we define a custom operator named 'M' that will multiply
# the second argument by 2.
-M = Operator(lambda a, b: a[b*2])
+M = Operator(lambda a, b: a[b * 2])
# or directly as function
# @Operator
# def M(a, b):
# return a[b*2]
+
@struct(options={S_REPLACE_TYPES})
class Format:
# __annotations__ should contain typing.List[int] as we've specified the
# documentation option.
- f1: f[list[int], uint16 /M/ 3]
+ f1: f[list[int], uint16 / M / 3]
-print(Format.__annotations__)
+print(Format.__annotations__)
diff --git a/examples/pointer_example.py b/examples/pointer_example.py
index 60e89b04..cbc219f7 100644
--- a/examples/pointer_example.py
+++ b/examples/pointer_example.py
@@ -7,12 +7,13 @@
S_REPLACE_TYPES,
unpack,
struct,
- x86
+ x86,
)
from caterpillar.shortcuts import f
set_struct_flags(S_REPLACE_TYPES)
+
@struct(kw_only=False, order=BigEndian)
class Format:
address: f[pointer[str], uintptr * CString(...)]
diff --git a/pyproject.toml b/pyproject.toml
index 5a4d31a0..cdd4927d 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -15,17 +15,17 @@ authors = [{ name = "MatrixEditor" }]
maintainers = [{ name = "MatrixEditor" }]
readme = "README.md"
classifiers = [
- "Development Status :: 4 - Beta",
- "Intended Audience :: Developers",
- "Intended Audience :: Science/Research",
+ "Development Status :: 4 - Beta",
+ "Intended Audience :: Developers",
+ "Intended Audience :: Science/Research",
- "License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
+ "License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
- 'Programming Language :: Python :: 3.10',
- 'Programming Language :: Python :: 3.11',
- 'Programming Language :: Python :: 3.12',
- 'Programming Language :: Python :: 3.13',
- 'Programming Language :: Python :: 3.14',
+ 'Programming Language :: Python :: 3.10',
+ 'Programming Language :: Python :: 3.11',
+ 'Programming Language :: Python :: 3.12',
+ 'Programming Language :: Python :: 3.13',
+ 'Programming Language :: Python :: 3.14',
]
# To ensure compatibility
dependencies = ["typing-extensions"]
@@ -53,6 +53,7 @@ dev = [
"black>=26.5.1",
"pytest>=9.1.1",
"tox>=4.56.1",
+ "pytest-benchmark>=5.2.3",
]
docs = [
"breathe>=4.36.0",
diff --git a/src/caterpillar/_C.pyi b/src/caterpillar/_C.pyi
index bcdd8668..3e20dc0e 100644
--- a/src/caterpillar/_C.pyi
+++ b/src/caterpillar/_C.pyi
@@ -15,22 +15,23 @@
# pyright: reportExplicitAny=false, reportAny=false, reportPrivateUsage=false
from collections.abc import Collection
from typing import Any, Generic
-from typing_extensions import override, Self
+
+from typing_extensions import Self, override
+
+from caterpillar import native_support
from caterpillar.abc import (
- _OT,
_IT,
- _EndianLike,
- _SupportsSetEndian,
+ _OT,
+ _ContextLambda,
_ContextLike,
+ _EndianLike,
_LengthT,
_StructLike,
- _ContextLambda,
+ _SupportsSetEndian,
_SwitchOptionsT,
)
-from caterpillar import native_support
if native_support():
-
BIG_ENDIAN: c_Endian
HOST_ARCH: c_Arch
LITTLE_ENDIAN: c_Endian
diff --git a/src/caterpillar/__init__.py b/src/caterpillar/__init__.py
index ae5b85d7..6babbd27 100644
--- a/src/caterpillar/__init__.py
+++ b/src/caterpillar/__init__.py
@@ -23,7 +23,8 @@ def native_support() -> bool:
"""Return True if native support is available."""
try:
# fmt: off
- from caterpillar import _C # pyright: ignore[reportMissingModuleSource, reportUnusedImport]
+ from caterpillar import \
+ _C # pyright: ignore[reportMissingModuleSource, reportUnusedImport]
return True
except Exception:
diff --git a/src/caterpillar/__main__.py b/src/caterpillar/__main__.py
index 077525da..7ee9b911 100644
--- a/src/caterpillar/__main__.py
+++ b/src/caterpillar/__main__.py
@@ -13,8 +13,8 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
# pyright: reportUnusedCallResult=false, reportAny=false
-import os
import argparse
+import os
parser = argparse.ArgumentParser()
parser.add_argument("--include-dir", action="store_true")
diff --git a/src/caterpillar/_common.py b/src/caterpillar/_common.py
old mode 100755
new mode 100644
index 9b5c4fad..9ae22fdb
--- a/src/caterpillar/_common.py
+++ b/src/caterpillar/_common.py
@@ -13,35 +13,34 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
# pyright: reportPrivateUsage=false, reportExplicitAny=false, reportCallIssue=false
-from collections.abc import Collection
import itertools
-
+from collections.abc import Collection
from types import TracebackType
from typing import TYPE_CHECKING, Any, Callable
from caterpillar.abc import (
- _PrefixedType,
- _ContextLike,
- _OT,
_IT,
- _StreamType,
+ _OT,
+ _ContextLike,
_LengthT,
+ _PrefixedType,
+ _StreamType,
_SupportsPack,
)
from caterpillar.context import (
- CTX_PATH,
CTX_FIELD,
CTX_INDEX,
CTX_OBJECT,
- CTX_STREAM,
+ CTX_PATH,
CTX_SEQ,
+ CTX_STREAM,
O_CONTEXT_FACTORY,
Context,
)
from caterpillar.exception import (
+ InvalidValueError,
Stop,
StructException,
- InvalidValueError,
ValidationError,
)
from caterpillar.options import O_ARRAY_FACTORY
@@ -56,7 +55,7 @@ def __init__(self, context: _ContextLike, name: str, value: Any) -> None:
self.old_value: Any = context[name]
self.value: Any = value
self.name: str = name
- self.field: "Field[Any, Any] | None" = context.get(CTX_FIELD)
+ self.field: Field[Any, Any] | None = context.get(CTX_FIELD)
def __enter__(self) -> None:
self.context[self.name] = self.value
diff --git a/src/caterpillar/abc.py b/src/caterpillar/abc.py
old mode 100755
new mode 100644
index 632c6ee2..7a755c2b
--- a/src/caterpillar/abc.py
+++ b/src/caterpillar/abc.py
@@ -13,12 +13,20 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
# pyright: reportExplicitAny=false, reportAny=false
-from collections.abc import Iterable, Collection
+from collections.abc import Callable, Collection, Iterable
from io import IOBase
from types import EllipsisType, NoneType
-from typing import Any, Callable, Protocol, runtime_checkable, TYPE_CHECKING
-from typing_extensions import Buffer, Final, Literal, TypeVar, overload, override
+from typing import (
+ TYPE_CHECKING,
+ Any,
+ Final,
+ Literal,
+ Protocol,
+ TypeAlias,
+ runtime_checkable,
+)
+from typing_extensions import Buffer, TypeVar, overload, override
if TYPE_CHECKING:
from caterpillar.py import Field
@@ -539,7 +547,7 @@ class _ArrayFactoryLike(Protocol[_IT]):
into a concrete collection type.
"""
- def __call__(self, __value: Iterable[_IT]) -> Collection[_IT]:
+ def __call__(self, /, __value: Iterable[_IT]) -> Collection[_IT]:
"""Create a collection from the given iterable.
:param __value: Iterable of input values.
@@ -572,26 +580,69 @@ def __call__(self, **kwargs: Any) -> _ContextLike:
_AnnotationT = str | bytes | type | _ActionLike | _StructLike | Any
+ContextLike: TypeAlias = _ContextLike
+ContextLambda: TypeAlias = _ContextLambda[_OT]
+SupportsActionUnpack: TypeAlias = _SupportsActionUnpack
+SupportsActionPack: TypeAlias = _SupportsActionPack
+ActionLike: TypeAlias = _ActionLike
+SupportsPack: TypeAlias = _SupportsPack[_IT]
+SupportsUnpack: TypeAlias = _SupportsUnpack[_OT]
+SupportsSize: TypeAlias = _SupportsSize
+StructLike: TypeAlias = _StructLike[_IT, _OT]
+SupportsType: TypeAlias = _SupportsType
+ContainsStruct: TypeAlias = _ContainsStruct[_IT, _OT]
+SwitchLambda: TypeAlias = _SwitchLambda
+SupportsBits: TypeAlias = _SupportsBits
+ContainsBits: TypeAlias = _ContainsBits
+ArchLike: TypeAlias = _ArchLike
+SupportsSetEndian: TypeAlias = _SupportsSetEndian[_IT]
+EndianLike: TypeAlias = _EndianLike
+OptionLike: TypeAlias = _OptionLike[_IT]
+ArrayFactoryLike: TypeAlias = _ArrayFactoryLike[_IT]
+ContextFactoryLike: TypeAlias = _ContextFactoryLike
+
__all__ = [
- "_ContextLike",
+ "ActionLike",
+ "ArchLike",
+ "ArrayFactoryLike",
+ "ContainsBits",
+ "ContainsStruct",
+ "ContextFactoryLike",
+ "ContextLambda",
+ "ContextLike",
+ "EndianLike",
+ "OptionLike",
+ "StructLike",
+ "SupportsActionPack",
+ "SupportsActionUnpack",
+ "SupportsBits",
+ "SupportsPack",
+ "SupportsSetEndian",
+ "SupportsSize",
+ "SupportsType",
+ "SupportsUnpack",
+ "SwitchLambda",
+ "_ActionLike",
+ "_AnnotationT",
+ "_ArchLike",
+ "_ArrayFactoryLike",
+ "_ContainsBits",
+ "_ContainsStruct",
+ "_ContextFactoryLike",
"_ContextLambda",
- "_SupportsActionUnpack",
+ "_ContextLike",
+ "_EndianLike",
+ "_LengthT",
+ "_OptionLike",
+ "_StructLike",
"_SupportsActionPack",
+ "_SupportsActionUnpack",
+ "_SupportsBits",
"_SupportsPack",
- "_SupportsUnpack",
+ "_SupportsSetEndian",
"_SupportsSize",
- "_StructLike",
"_SupportsType",
- "_ContainsStruct",
+ "_SupportsUnpack",
"_SwitchLambda",
- "_SupportsBits",
- "_ContainsBits",
- "_ArrayFactoryLike",
- "_OptionLike",
- "_ArchLike",
- "_ContextFactoryLike",
- "_ActionLike",
"_SwitchOptionsT",
- "_LengthT",
- "_AnnotationT",
]
diff --git a/src/caterpillar/byteorder.py b/src/caterpillar/byteorder.py
old mode 100755
new mode 100644
index 88f3c8cc..601e04ae
--- a/src/caterpillar/byteorder.py
+++ b/src/caterpillar/byteorder.py
@@ -14,25 +14,26 @@
# along with this program. If not, see .
# pyright: reportPrivateUsage=false
import inspect
-
-from sys import maxsize
-from platform import machine
+from collections.abc import Callable
from dataclasses import dataclass
from enum import Enum
-from typing import Callable
-from typing_extensions import Final, override
+from platform import machine
+from sys import maxsize
+from typing import Final
+
+from typing_extensions import override
from caterpillar.abc import (
- _EndianLike,
- _SupportsSetEndian,
_OT,
- _ContextLambda,
- _ContextLike,
- _ArchLike,
+ ArchLike,
+ ContextLambda,
+ ContextLike,
+ EndianLike,
+ SupportsSetEndian,
)
-from caterpillar.shared import ATTR_BYTEORDER
from caterpillar.context import CTX_ORDER
from caterpillar.options import Flag
+from caterpillar.shared import ATTR_BYTEORDER
@dataclass(frozen=True)
@@ -72,7 +73,7 @@ def apply(self, other: object) -> None:
"""
setattr(other, ATTR_BYTEORDER, self)
- def __add__(self, other: _SupportsSetEndian[_OT]) -> _OT:
+ def __add__(self, other: SupportsSetEndian[_OT]) -> _OT:
"""
Adds the byte order information to another object using the
`__set_byteorder__` method.
@@ -152,23 +153,23 @@ class Format:
def __init__(
self,
name: str | None = None,
- key: str | _ContextLambda[str | _EndianLike | bool] | None = None,
- func: Callable[[], _EndianLike] | _ContextLambda[_EndianLike] | None = None,
+ key: str | ContextLambda[str | EndianLike | bool] | None = None,
+ func: Callable[[], EndianLike] | ContextLambda[EndianLike] | None = None,
init_ch: str | None = None,
) -> None:
self.name: str = name or ""
self.__ch: str = init_ch or LITTLE_ENDIAN_FMT
- self.func: Callable[[], _EndianLike] | _ContextLambda[_EndianLike] | None = func
+ self.func: Callable[[], EndianLike] | ContextLambda[EndianLike] | None = func
self._ctx_func: bool = False
if func is not None:
spec = inspect.getfullargspec(func)
if len(spec.args) == 1:
self._ctx_func = True
- self.key: str | _ContextLambda[str | _EndianLike | bool] | None = key
+ self.key: str | ContextLambda[str | EndianLike | bool] | None = key
self.__key_str: bool = isinstance(self.key, str)
def __call__(
- self, key: str | _ContextLambda[str | _EndianLike | bool]
+ self, key: str | ContextLambda[str | EndianLike | bool]
) -> "DynByteOrder":
"""Create a derived dynamic byte order bound to a new key.
@@ -179,7 +180,7 @@ def __call__(
"""
return DynByteOrder(self.name, key, self.func, self.__ch)
- def getch(self, context: _ContextLike) -> str:
+ def getch(self, context: ContextLike) -> str:
"""Resolve the byte order format character from a context.
Resolution order:
@@ -208,7 +209,7 @@ def getch(self, context: _ContextLike) -> str:
if self.__key_str:
byte_order = context.__context_getattr__(self.key) # pyright: ignore[reportArgumentType]
else:
- byte_order: str | _EndianLike | bool = self.key(context) # pyright: ignore[reportCallIssue]
+ byte_order: str | EndianLike | bool = self.key(context) # pyright: ignore[reportCallIssue]
else:
root_context = context._root
byte_order = (root_context or {}).get(CTX_ORDER, SysNative)
@@ -232,7 +233,7 @@ def ch(self) -> str:
frame = inspect.currentframe()
if frame is not None and frame.f_back is not None:
ctx_frame = frame.f_back
- context: _ContextLike | None = ctx_frame.f_locals.get("context")
+ context: ContextLike | None = ctx_frame.f_locals.get("context")
if context is not None:
self.__ch = self.getch(context)
return self.__ch
@@ -248,7 +249,7 @@ def ch(self, value: str) -> None:
"""
self.__ch = value
- def __add__(self, other: _SupportsSetEndian[_OT]) -> _OT:
+ def __add__(self, other: SupportsSetEndian[_OT]) -> _OT:
"""Apply this byte order to another object.
Delegates to the other object's byte order assignment method.
@@ -265,6 +266,51 @@ def __repr__(self) -> str:
return f""
+class _InheritByteOrder(DynByteOrder):
+ """Byte order that resolves to the byte order of the enclosing struct.
+
+ Unlike :data:`Dynamic`, which always resolves against the *root*
+ context (see :meth:`DynByteOrder.getch`), this endian type resolves
+ against the *immediate* enclosing context. This allows a struct to
+ inherit the byte order of whichever struct embeds it, regardless of
+ nesting depth, instead of always deferring to the top-level order::
+
+ @struct(order=Inherit)
+ class Inner:
+ value: uint32
+
+ @struct(order=BigEndian)
+ class Outer:
+ inner: Inner # inner.value uses BigEndian
+
+ A struct declared with ``order=Inherit`` that has no enclosing struct
+ (e.g. used standalone, or passed directly to :func:`pack`/
+ :func:`unpack`) falls back to the regular default byte order
+ resolution (``O_DEFAULT_ENDIAN`` or :data:`LittleEndian`), exactly
+ like a struct with ``order=None``.
+
+ .. note::
+ Only structs explicitly declared with ``order=Inherit`` opt into
+ this behavior. The default (``order=None``) is unaffected and
+ keeps resolving to :data:`LittleEndian`/``O_DEFAULT_ENDIAN``
+ regardless of the enclosing struct's byte order.
+ """
+
+ def __init__(self) -> None:
+ super().__init__(name="")
+
+ @override
+ def getch(self, context: ContextLike) -> str:
+ order = context.get(CTX_ORDER)
+ if order is None or order is self:
+ # No byte order was propagated from an enclosing struct (e.g.
+ # standalone/top-level usage).
+ order = O_DEFAULT_ENDIAN.value or LittleEndian
+
+ ch = getattr(order, "ch", order)
+ return ch if isinstance(ch, str) else LITTLE_ENDIAN_FMT
+
+
LITTLE_ENDIAN_FMT: Final[str] = "<"
"""Struct format character representing little-endian byte order."""
@@ -295,11 +341,17 @@ def __repr__(self) -> str:
"""Predefined :class:`DynByteOrder` instance for runtime-determined byte order.
"""
-O_DEFAULT_ENDIAN: Final[Flag[_EndianLike]] = Flag("option.endian", value=None)
+Inherit: Final[_InheritByteOrder] = _InheritByteOrder()
+""":class:`DynByteOrder` instance that makes a struct inherit its
+byte order from the struct that embeds it (see :class:`_InheritByteOrder`),
+instead of using a fixed byte order.
+"""
+
+O_DEFAULT_ENDIAN: Final[Flag[EndianLike]] = Flag("option.endian", value=None)
"""Default flag option representing an unspecified byte order."""
-def byteorder(obj: object, default: _EndianLike | None = None) -> _EndianLike:
+def byteorder(obj: object, default: EndianLike | None = None) -> EndianLike:
"""
Get the byte order of an object, defaulting to SysNative if not explicitly set.
@@ -348,4 +400,4 @@ class Arch:
AMD: Final[Arch] = Arch("AMD", 32)
AMD64: Final[Arch] = Arch("AMD64", 64)
-O_DEFAULT_ARCH: Final[Flag[_ArchLike]] = Flag("option.arch", system_arch)
+O_DEFAULT_ARCH: Final[Flag[ArchLike]] = Flag("option.arch", system_arch)
diff --git a/src/caterpillar/context.py b/src/caterpillar/context.py
old mode 100755
new mode 100644
index 2fc186b5..ef9d9cdc
--- a/src/caterpillar/context.py
+++ b/src/caterpillar/context.py
@@ -13,40 +13,39 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
# pyright: reportPrivateUsage=false, reportExplicitAny=false, reportAny=false
-from __future__ import annotations
-
import operator
import sys
import typing
import warnings
-
-from typing import Annotated, Callable, Any, Generic, Protocol, get_args, get_origin
-from typing_extensions import (
- Buffer,
+from collections.abc import Callable, Sized
+from dataclasses import dataclass
+from types import FrameType, TracebackType
+from typing import (
+ Annotated,
+ Any,
Final,
+ Generic,
Literal,
- Self,
- Sized,
- overload,
- override,
- TypeVar,
+ Protocol,
+ get_args,
+ get_origin,
)
-from types import FrameType, TracebackType
-from dataclasses import dataclass
-from caterpillar.exception import StructException
-from caterpillar.registry import to_struct
-from caterpillar.options import Flag
+from typing_extensions import Buffer, Self, TypeVar, overload, override
+
from caterpillar.abc import (
- _ContextLike,
- _ContextLambda,
_IT,
- _ContextFactoryLike,
_OT,
- _StreamType,
- _EndianLike,
_ArchLike,
+ _ContextFactoryLike,
+ _ContextLambda,
+ _ContextLike,
+ _EndianLike,
+ _StreamType,
)
+from caterpillar.exception import StructException
+from caterpillar.options import Flag
+from caterpillar.registry import to_struct
from caterpillar.shared import iscond
if typing.TYPE_CHECKING:
@@ -279,78 +278,78 @@ class ExprMixin:
A mixin class providing methods for creating binary and unary expressions.
"""
- def __add__(self, other: object) -> BinaryExpression:
+ def __add__(self, other: object) -> "BinaryExpression":
return BinaryExpression(operator.add, self, other)
- def __sub__(self, other: object) -> BinaryExpression:
+ def __sub__(self, other: object) -> "BinaryExpression":
return BinaryExpression(operator.sub, self, other)
- def __mul__(self, other: object) -> BinaryExpression:
+ def __mul__(self, other: object) -> "BinaryExpression":
return BinaryExpression(operator.mul, self, other)
- def __floordiv__(self, other: object) -> BinaryExpression:
+ def __floordiv__(self, other: object) -> "BinaryExpression":
return BinaryExpression(operator.floordiv, self, other)
- def __truediv__(self, other: object) -> BinaryExpression:
+ def __truediv__(self, other: object) -> "BinaryExpression":
return BinaryExpression(operator.truediv, self, other)
- def __mod__(self, other: object) -> BinaryExpression:
+ def __mod__(self, other: object) -> "BinaryExpression":
return BinaryExpression(operator.mod, self, other)
- def __pow__(self, other: object) -> BinaryExpression:
+ def __pow__(self, other: object) -> "BinaryExpression":
return BinaryExpression(operator.pow, self, other)
- def __xor__(self, other: object) -> BinaryExpression:
+ def __xor__(self, other: object) -> "BinaryExpression":
return BinaryExpression(operator.xor, self, other)
- def __and__(self, other: object) -> BinaryExpression:
+ def __and__(self, other: object) -> "BinaryExpression":
return BinaryExpression(operator.and_, self, other)
- def __or__(self, other: object) -> BinaryExpression:
+ def __or__(self, other: object) -> "BinaryExpression":
return BinaryExpression(operator.or_, self, other)
- def __rshift__(self, other: object) -> BinaryExpression:
+ def __rshift__(self, other: object) -> "BinaryExpression":
return BinaryExpression(operator.rshift, self, other)
- def __lshift__(self, other: object) -> BinaryExpression:
+ def __lshift__(self, other: object) -> "BinaryExpression":
return BinaryExpression(operator.lshift, self, other)
__div__ = __truediv__ # pyright: ignore[reportUnannotatedClassAttribute]
- def __radd__(self, other: object) -> BinaryExpression:
+ def __radd__(self, other: object) -> "BinaryExpression":
return BinaryExpression(operator.add, other, self)
- def __rsub__(self, other: object) -> BinaryExpression:
+ def __rsub__(self, other: object) -> "BinaryExpression":
return BinaryExpression(operator.sub, other, self)
- def __rmul__(self, other: object) -> BinaryExpression:
+ def __rmul__(self, other: object) -> "BinaryExpression":
return BinaryExpression(operator.mul, other, self)
- def __rfloordiv__(self, other: object) -> BinaryExpression:
+ def __rfloordiv__(self, other: object) -> "BinaryExpression":
return BinaryExpression(operator.floordiv, other, self)
- def __rtruediv__(self, other: object) -> BinaryExpression:
+ def __rtruediv__(self, other: object) -> "BinaryExpression":
return BinaryExpression(operator.truediv, other, self)
- def __rmod__(self, other: object) -> BinaryExpression:
+ def __rmod__(self, other: object) -> "BinaryExpression":
return BinaryExpression(operator.mod, other, self)
- def __rpow__(self, other: object) -> BinaryExpression:
+ def __rpow__(self, other: object) -> "BinaryExpression":
return BinaryExpression(operator.pow, other, self)
- def __rxor__(self, other: object) -> BinaryExpression:
+ def __rxor__(self, other: object) -> "BinaryExpression":
return BinaryExpression(operator.xor, other, self)
- def __rand__(self, other: object) -> BinaryExpression:
+ def __rand__(self, other: object) -> "BinaryExpression":
return BinaryExpression(operator.and_, other, self)
- def __ror__(self, other: object) -> BinaryExpression:
+ def __ror__(self, other: object) -> "BinaryExpression":
return BinaryExpression(operator.or_, other, self)
- def __rrshift__(self, other: object) -> BinaryExpression:
+ def __rrshift__(self, other: object) -> "BinaryExpression":
return BinaryExpression(operator.rshift, other, self)
- def __rlshift__(self, other: object) -> BinaryExpression:
+ def __rlshift__(self, other: object) -> "BinaryExpression":
return BinaryExpression(operator.lshift, other, self)
def __neg__(self):
@@ -362,31 +361,31 @@ def __pos__(self):
def __invert__(self):
return UnaryExpression("invert", operator.invert, self)
- def __contains__(self, other: object) -> BinaryExpression:
+ def __contains__(self, other: object) -> "BinaryExpression":
return BinaryExpression(operator.contains, self, other)
- def __gt__(self, other: object) -> BinaryExpression:
+ def __gt__(self, other: object) -> "BinaryExpression":
return BinaryExpression(operator.gt, self, other)
- def __ge__(self, other: object) -> BinaryExpression:
+ def __ge__(self, other: object) -> "BinaryExpression":
return BinaryExpression(operator.ge, self, other)
- def __lt__(self, other: object) -> BinaryExpression:
+ def __lt__(self, other: object) -> "BinaryExpression":
return BinaryExpression(operator.lt, self, other)
- def __le__(self, other: object) -> BinaryExpression:
+ def __le__(self, other: object) -> "BinaryExpression":
return BinaryExpression(operator.le, self, other)
@override
def __eq__( # pyright: ignore[reportIncompatibleMethodOverride]
self, other: object
- ) -> BinaryExpression:
+ ) -> "BinaryExpression":
return BinaryExpression(operator.eq, self, other)
@override
def __ne__( # pyright: ignore[reportIncompatibleMethodOverride]
self, other: object
- ) -> BinaryExpression:
+ ) -> "BinaryExpression":
return BinaryExpression(operator.ne, self, other)
@@ -415,10 +414,10 @@ class Format:
"""
__slots__: tuple[str, ...] = (
- "func",
"annotations",
- "namelist",
"depth",
+ "func",
+ "namelist",
)
def __init__(self, condition: _ContextLambda[bool] | bool, depth: int = 2):
@@ -650,37 +649,41 @@ def __type__(self) -> type:
@overload
def __getattribute__(
self, key: Literal["_field"]
- ) -> ContextPath["Field[_IT, _OT] | None"]: ...
+ ) -> "ContextPath[Field[_IT, _OT] | None]": ...
@overload
def __getattribute__(
self, key: Literal["_parent"]
- ) -> ContextPath[_ContextLike]: ...
+ ) -> "ContextPath[_ContextLike]": ...
@overload
- def __getattribute__(self, key: Literal["_obj"]) -> ContextPath[_ContextLike]: ...
+ def __getattribute__(self, key: Literal["_obj"]) -> "ContextPath[_ContextLike]": ...
@overload
def __getattribute__(
self, key: Literal["_offsets"]
- ) -> ContextPath[dict[int, Buffer]]: ...
+ ) -> "ContextPath[dict[int, Buffer]]": ...
@overload
- def __getattribute__(self, key: Literal["_io"]) -> ContextPath[_StreamType]: ...
+ def __getattribute__(self, key: Literal["_io"]) -> "ContextPath[_StreamType]": ...
@overload
- def __getattribute__(self, key: Literal["_pos"]) -> ContextPath[int]: ...
+ def __getattribute__(self, key: Literal["_pos"]) -> "ContextPath[int]": ...
@overload
- def __getattribute__(self, key: Literal["_index"]) -> ContextPath[int]: ...
+ def __getattribute__(self, key: Literal["_index"]) -> "ContextPath[int]": ...
@overload
- def __getattribute__(self, key: Literal["_path"]) -> ContextPath[str]: ...
+ def __getattribute__(self, key: Literal["_path"]) -> "ContextPath[str]": ...
@overload
- def __getattribute__(self, key: Literal["_is_seq"]) -> ContextPath[bool]: ...
+ def __getattribute__(self, key: Literal["_is_seq"]) -> "ContextPath[bool]": ...
@overload
- def __getattribute__(self, key: Literal["_root"]) -> ContextPath[_ContextLike]: ...
+ def __getattribute__(
+ self, key: Literal["_root"]
+ ) -> "ContextPath[_ContextLike]": ...
@overload
- def __getattribute__(self, key: Literal["_order"]) -> ContextPath[_EndianLike]: ...
+ def __getattribute__(
+ self, key: Literal["_order"]
+ ) -> "ContextPath[_EndianLike]": ...
@overload
- def __getattribute__(self, key: Literal["_arch"]) -> ContextPath[_ArchLike]: ...
+ def __getattribute__(self, key: Literal["_arch"]) -> "ContextPath[_ArchLike]": ...
@overload
- def __getattribute__(self, key: str) -> ContextPath[_IT]: ...
+ def __getattribute__(self, key: str) -> "ContextPath[_IT]": ...
@override
- def __getattribute__(self, key: str) -> ContextPath[_IT]:
+ def __getattribute__(self, key: str) -> "ContextPath[_IT]":
"""
Gets an attribute from the ContextPath, creating a new instance if needed.
@@ -725,14 +728,22 @@ def __str__(self) -> str:
return self.path or ""
@property
- def parent(self) -> ContextPath[_ContextLike]:
- path = f"{CTX_PARENT}.{CTX_OBJECT}"
- if not self.path:
- return ContextPath(path)
- return ContextPath(".".join([self.path, path]))
+ def parent(self) -> "ContextPath[_ContextLike]":
+ """
+ .. versionchanged:: 2.9.2
+ Always resolves to the parent context object instead the parent context.
+ """
+ if not self.path or self.path == CTX_OBJECT:
+ return ContextPath(f"{CTX_PARENT}.{CTX_OBJECT}")
+
+ context_path = self.path
+ if self.path.endswith(f".{CTX_OBJECT}"):
+ context_path = self.path[: -len(CTX_OBJECT)].rstrip(".")
+
+ return ContextPath(f"{context_path}.{CTX_PARENT}.{CTX_OBJECT}")
@property
- def parentctx(self) -> ContextPath[_ContextLike]:
+ def parentctx(self) -> "ContextPath[_ContextLike]":
"""
.. versionadded:: 2.8.0
"""
diff --git a/src/caterpillar/fields/__init__.py b/src/caterpillar/fields/__init__.py
index e4728042..572caa4f 100644
--- a/src/caterpillar/fields/__init__.py
+++ b/src/caterpillar/fields/__init__.py
@@ -12,139 +12,139 @@
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
-from ._base import Field, INVALID_DEFAULT, DEFAULT_OPTION, singleton
-from ._mixin import FieldMixin, FieldStruct, Chain, Operator, get_args, get_kwargs
+from ._base import DEFAULT_OPTION, INVALID_DEFAULT, Field, singleton
+from ._mixin import Chain, FieldMixin, FieldStruct, Operator, get_args, get_kwargs
from .common import (
- PyStructFormattedField,
- Transformer,
+ ENUM_STRICT,
+ Aligned,
+ AsLengthRef,
+ Bytes,
+ Computed,
Const,
ConstBytes,
ConstString,
+ CString,
Enum,
- String,
- Bytes,
+ Int,
+ Lazy,
Memory,
- Computed,
+ Padded,
+ Padding,
Pass,
- CString,
+ PostPad,
Prefixed,
- Int,
- UInt,
- Padding,
- AsLengthRef,
+ PrePad,
+ PyStructFormattedField,
+ String,
Timestamp,
- padding,
- char,
+ Transformer,
+ UInt,
+ Uuid,
+ align,
boolean,
+ char,
+ double,
+ float16,
+ float32,
+ float64,
int8,
- uint8,
int16,
- uint16,
int24,
- uint24,
int32,
- uint32,
int64,
- uint64,
- pssize,
+ padding,
psize,
- float16,
- float32,
- float64,
- double,
+ pssize,
+ uint8,
+ uint16,
+ uint24,
+ uint32,
+ uint64,
void_ptr,
- Uuid,
- Aligned,
- align,
- Lazy,
- ENUM_STRICT,
- Padded,
- PostPad,
- PrePad,
)
-from .varint import VarInt, VARINT_LSB, vint
from .compression import (
- Compressed,
- ZLibCompressed,
Bz2Compressed,
+ Compressed,
LZMACompressed,
LZOCompressed,
-)
-from .crypto import Encrypted, Xor, Or, And, KeyCipher
-from .net import MAC, MACAddress, IPv4Address, IPv6Address
-from .pointer import (
- uintptr,
- intptr,
- offintptr,
- offuintptr,
- Pointer,
- pointer,
- intptr_fn,
- PTR_STRICT,
- relative_pointer,
- RelativePointer,
- uintptr_fn,
+ ZLibCompressed,
)
from .conditional import (
+ Branch,
ConditionalChain,
- If,
Else,
ElseIf,
- When,
- Branch,
End,
- Start,
+ If,
Otherwise,
+ Start,
+ When,
)
-from .hook import IOHook
+from .crypto import And, Encrypted, KeyCipher, Or, Xor
from .digest import (
- Digest,
+ CTX_DIGEST,
+ CTX_DIGEST_ALGO,
+ CTX_DIGEST_HOOK,
+ CTX_DIGEST_OBJ,
+ HMAC,
+ Adler,
+ Adler_Algo,
+ Adler_Field,
Algorithm,
- Md5,
- Sha1,
- Sha2_256,
- Sha2_224,
- Sha2_384,
- Sha2_512,
- Sha3_224,
- Sha3_256,
- Sha3_384,
- Sha3_512,
Crc32,
- Adler,
- HMAC,
+ Crc32_Algo,
+ Crc32_Field,
+ Digest,
DigestField,
DigestFieldAction,
+ HMACAlgorithm,
+ Md5,
Md5_Algo,
Md5_Field,
+ Sha1,
Sha1_Algo,
Sha1_Field,
- Sha2_256_Algo,
- Sha2_256_Field,
+ Sha2_224,
Sha2_224_Algo,
Sha2_224_Field,
+ Sha2_256,
+ Sha2_256_Algo,
+ Sha2_256_Field,
+ Sha2_384,
Sha2_384_Algo,
Sha2_384_Field,
+ Sha2_512,
Sha2_512_Algo,
Sha2_512_Field,
+ Sha3_224,
Sha3_224_Algo,
Sha3_224_Field,
+ Sha3_256,
Sha3_256_Algo,
Sha3_256_Field,
+ Sha3_384,
Sha3_384_Algo,
Sha3_384_Field,
+ Sha3_512,
Sha3_512_Algo,
Sha3_512_Field,
- Crc32_Algo,
- Crc32_Field,
- Adler_Algo,
- Adler_Field,
- CTX_DIGEST,
- CTX_DIGEST_ALGO,
- CTX_DIGEST_HOOK,
- CTX_DIGEST_OBJ,
- HMACAlgorithm,
)
+from .hook import IOHook
+from .net import MAC, IPv4Address, IPv6Address, MACAddress
+from .pointer import (
+ PTR_STRICT,
+ Pointer,
+ RelativePointer,
+ intptr,
+ intptr_fn,
+ offintptr,
+ offuintptr,
+ pointer,
+ relative_pointer,
+ uintptr,
+ uintptr_fn,
+)
+from .varint import VARINT_LSB, VarInt, vint
__all__ = [
"Adler_Algo",
diff --git a/src/caterpillar/fields/_base.py b/src/caterpillar/fields/_base.py
old mode 100755
new mode 100644
index 94802fad..ea850522
--- a/src/caterpillar/fields/_base.py
+++ b/src/caterpillar/fields/_base.py
@@ -13,26 +13,28 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
# pyright: reportPrivateUsage=false, reportAny=false, reportExplicitAny=false
-from io import BytesIO
from collections.abc import Collection
+from io import BytesIO
from typing import Any, Generic, get_origin
-from typing_extensions import Self, override, TypeVar
+from typing_extensions import Self, TypeVar, override
+
+from caterpillar import registry
from caterpillar.abc import (
- _StructLike,
- _GreedyType,
- _PrefixedType,
_IT,
_OT,
- _EndianLike,
_ArchLike,
- _ContextLike,
- _ContextLambda,
- _OptionLike,
_ContainsStruct,
- _SwitchOptionsT,
+ _ContextLambda,
+ _ContextLike,
+ _EndianLike,
+ _GreedyType,
_LengthT,
+ _OptionLike,
+ _PrefixedType,
_StreamType,
+ _StructLike,
+ _SwitchOptionsT,
)
from caterpillar.byteorder import (
O_DEFAULT_ARCH,
@@ -40,21 +42,16 @@
LittleEndian,
system_arch,
)
+from caterpillar.context import CTX_FIELD, CTX_OFFSETS, CTX_SEQ, CTX_STREAM, CTX_VALUE
from caterpillar.exception import (
DynamicSizeError,
- StructException,
+ InvalidValueError,
OptionError,
+ StructException,
ValidationError,
- InvalidValueError,
)
-from caterpillar.options import (
- GLOBAL_FIELD_FLAGS,
- F_DYNAMIC,
-)
-from caterpillar.context import CTX_OFFSETS, CTX_STREAM, CTX_FIELD, CTX_VALUE, CTX_SEQ
-from caterpillar import registry
-from caterpillar.shared import getstruct, typeof, PackMixin, UnpackMixin
-
+from caterpillar.options import F_DYNAMIC, GLOBAL_FIELD_FLAGS
+from caterpillar.shared import PackMixin, UnpackMixin, getstruct, typeof
_T = TypeVar("_T")
@@ -250,9 +247,7 @@ def options(self, value: _SwitchOptionsT | None):
self.__options = value
self._switch_is_lambda = callable(value)
self._switch_has_default = (
- bool(value)
- and not self._switch_is_lambda
- and DEFAULT_OPTION in value # pyright: ignore[reportOperatorIssue]
+ bool(value) and not self._switch_is_lambda and DEFAULT_OPTION in value # pyright: ignore[reportOperatorIssue]
)
@property
@@ -578,6 +573,10 @@ def __unpack__(self, context: _ContextLike) -> _OT:
# The "keep_position" flag is not applicable here. Configure a field to keep the
# position afterward.
context[CTX_VALUE] = value
+ # A discriminator may itself be a Field and replace the active
+ # field context. The selected target is still embedded by self.
+ context[CTX_FIELD] = self
+ context[CTX_SEQ] = self._is_seq
value = struct.__unpack__(context)
# fmt; on
return value
@@ -639,6 +638,8 @@ def __pack__(self, obj: _IT, context: _ContextLike) -> None:
# support for non-context lambdas with switch statements
self.struct.__pack__(value, context)
if self.options is not None:
+ context[CTX_FIELD] = self
+ context[CTX_SEQ] = self._is_seq
struct = self.get_struct(value, context)
struct.__pack__(obj, context)
diff --git a/src/caterpillar/fields/_mixin.py b/src/caterpillar/fields/_mixin.py
old mode 100755
new mode 100644
index 7d647527..66df01be
--- a/src/caterpillar/fields/_mixin.py
+++ b/src/caterpillar/fields/_mixin.py
@@ -13,29 +13,31 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
# pyright: reportPrivateUsage=false, reportExplicitAny=false, reportAny=false
-from io import BytesIO
from collections.abc import Collection, Iterable
from functools import partial
+from io import BytesIO
from typing import Any, Callable, Generic
+
from typing_extensions import TypeVar, overload, override
-from caterpillar.byteorder import byteorder
-from caterpillar.options import Flag
-from caterpillar.context import CTX_SEQ, CTX_STREAM
-from caterpillar._common import unpack_seq, pack_seq, WithoutContextVar
-from caterpillar.shared import PackMixin, UnpackMixin, getstruct
+from caterpillar._common import WithoutContextVar, pack_seq, unpack_seq
from caterpillar.abc import (
+ _IT,
+ _OT,
+ _ArgType,
_ContextLambda,
_ContextLike,
- _StructLike,
+ _EndianLike,
_LengthT,
- _IT,
- _OT,
_OptionLike,
- _EndianLike,
+ _StructLike,
_SwitchOptionsT,
- _ArgType,
)
+from caterpillar.byteorder import byteorder
+from caterpillar.context import CTX_SEQ, CTX_STREAM
+from caterpillar.options import Flag
+from caterpillar.shared import PackMixin, UnpackMixin, getstruct
+
from ._base import Field
@@ -68,9 +70,7 @@ def __getitem__(self, dim: _LengthT) -> Field[Collection[_IT], Collection[_OT]]:
# fmt: off
return Field(self, byteorder(self))[dim] # pyright: ignore[reportArgumentType]
- def __rshift__(
- self, switch: _SwitchOptionsT
- ) -> Field[_IT, _OT]:
+ def __rshift__(self, switch: _SwitchOptionsT) -> Field[_IT, _OT]:
"""Inserts switch options into the new field"""
# fmt: off
return Field(self, byteorder(self)) >> switch # pyright: ignore[reportArgumentType]
@@ -90,15 +90,14 @@ def __and__(
self, other: "_StructLike[_OT, _ChainTailT]"
) -> "Chain[_IT, _ChainTailT]": ...
@overload
- def __and__(
- self, other: "type[_ChainTailT]"
- ) -> "Chain[_IT, _ChainTailT]": ...
+ def __and__(self, other: "type[_ChainTailT]") -> "Chain[_IT, _ChainTailT]": ...
@overload
def __and__(
self, other: "Chain[_OT, _ChainTailT]"
) -> "Chain[_IT, _ChainTailT]": ...
def __and__(
- self, other: "Chain[_OT, _ChainTailT] | _StructLike[_OT, _ChainTailT] | type[_OT]"
+ self,
+ other: "Chain[_OT, _ChainTailT] | _StructLike[_OT, _ChainTailT] | type[_OT]",
) -> "Chain":
"""Returns a chain joining this structure before the next element or chain."""
# fmt: off
diff --git a/src/caterpillar/fields/common.py b/src/caterpillar/fields/common.py
old mode 100755
new mode 100644
index ea25ecc1..c328d2a0
--- a/src/caterpillar/fields/common.py
+++ b/src/caterpillar/fields/common.py
@@ -16,53 +16,44 @@
import datetime
import struct as PyStruct
import warnings
-
+from collections.abc import Callable, Collection
+from enum import Enum as _EnumType
+from functools import cached_property
from io import BytesIO
-from typing import Any, Callable, Generic
+from types import NoneType
+from typing import Any, Final, Generic
+from uuid import UUID
+
from typing_extensions import (
Buffer,
- Final,
Self,
SupportsFloat,
SupportsIndex,
- override,
TypeVar,
+ override,
)
-from types import NoneType
-from functools import cached_property
-from enum import Enum as _EnumType
-from uuid import UUID
-from collections.abc import Collection
+from caterpillar import registry
+from caterpillar._common import WithoutContextVar, read_exact
from caterpillar.abc import (
- _StructLike,
- _StreamType,
- _ContextLike,
- _PrefixedType,
- _ContextLambda,
_IT,
_OT,
- _LengthT,
- _EndianLike,
+ ContextLambda,
+ ContextLike,
_GreedyType,
+ _LengthT,
+ _PrefixedType,
+ _StreamType,
+ _StructLike,
+ EndianLike,
)
-from caterpillar.exception import (
- ValidationError,
- InvalidValueError,
- DynamicSizeError,
-)
-from caterpillar.context import CTX_FIELD, CTX_STREAM, CTX_SEQ
-from caterpillar.options import Flag, GLOBAL_FIELD_FLAGS
-from caterpillar.byteorder import (
- LITTLE_ENDIAN_FMT,
- O_DEFAULT_ENDIAN,
- LittleEndian,
-)
-from caterpillar import registry
-from caterpillar._common import WithoutContextVar, read_exact
-from caterpillar.shared import getstruct, typeof
+from caterpillar.byteorder import LITTLE_ENDIAN_FMT, O_DEFAULT_ENDIAN, LittleEndian
+from caterpillar.context import CTX_FIELD, CTX_ORDER, CTX_SEQ, CTX_STREAM
+from caterpillar.exception import DynamicSizeError, InvalidValueError, ValidationError
+from caterpillar.options import GLOBAL_FIELD_FLAGS, Flag
+from caterpillar.shared import ATTR_BYTEORDER, getstruct, typeof
-from ._base import Field, INVALID_DEFAULT, singleton
+from ._base import INVALID_DEFAULT, Field, singleton
from ._mixin import ByteOrderMixin, FieldStruct
# Explicitly report deprecation warnings
@@ -73,6 +64,22 @@
_NATIVE_ONLY_FORMATS: Final[frozenset[str]] = frozenset({"n", "N", "P"})
+def resolve_order(
+ context: ContextLike, obj: object, field: Field | None = None
+) -> EndianLike:
+ field = field or context.get(CTX_FIELD)
+ return (
+ field.order
+ if field and field.has_order()
+ else (
+ context.get(CTX_ORDER)
+ or getattr(obj, ATTR_BYTEORDER, None)
+ or O_DEFAULT_ENDIAN.value
+ or LittleEndian
+ )
+ )
+
+
class PyStructFormattedField(FieldStruct[_IT, _IT]):
"""
A field class representing a binary format using format characters (e.g., 'i', 'I', etc.).
@@ -99,7 +106,7 @@ def __init__(self, ch: str, type_: type) -> None:
# tiny hack to reduce some PyStruct.Struct instantiations
self._cache: dict[str, PyStruct.Struct] = {}
self.__bits__: int = PyStruct.calcsize(self.text) * 8
- self.__byteorder__: _EndianLike | None = None
+ self.__byteorder__: EndianLike | None = None
if self.text == "x":
warnings.warn(
"Python struct's padding is not supported anymore (since 2.8.1). Use the Padding class instead.",
@@ -144,7 +151,7 @@ def __type__(self) -> type:
"""
return self.ty
- def __size__(self, context: _ContextLike) -> int:
+ def __size__(self, context: ContextLike) -> int:
"""
Calculate the size of the field in bytes.
@@ -157,7 +164,7 @@ def __size__(self, context: _ContextLike) -> int:
return self.__bits__ // 8
@override
- def pack_single(self, obj: _IT, context: _ContextLike) -> None:
+ def pack_single(self, obj: _IT, context: ContextLike) -> None:
"""
Pack a single value into the stream using the defined format character.
@@ -167,16 +174,11 @@ def pack_single(self, obj: _IT, context: _ContextLike) -> None:
if obj is None:
return # Skip packing if the value is None and the field is not padding
- field = context.get(CTX_FIELD)
- order_ch = (
- field.order.ch
- if field
- else (self.__byteorder__ or O_DEFAULT_ENDIAN.value or LittleEndian).ch
- )
+ order_ch = resolve_order(context, self).ch
context[CTX_STREAM].write(self._cached(order_ch).pack(obj))
@override
- def pack_seq(self, seq: Collection[_IT], context: _ContextLike) -> None:
+ def pack_seq(self, seq: Collection[_IT], context: ContextLike) -> None:
"""
Pack a sequence of values into the stream.
@@ -192,7 +194,7 @@ def pack_seq(self, seq: Collection[_IT], context: _ContextLike) -> None:
return # nothing to do
# just pack directly
# WE LOSE SIZE CHECKING HERE!
- ch = (self.__byteorder__ or O_DEFAULT_ENDIAN.value or LittleEndian).ch
+ ch = resolve_order(context, self).ch
struct_ = self._cached(ch, target_length)
else:
length = field.length(context)
@@ -211,24 +213,20 @@ def pack_seq(self, seq: Collection[_IT], context: _ContextLike) -> None:
if target_length == 0:
return # nothing to do
- struct_ = self._cached(field.order.ch, target_length)
+ ch = resolve_order(context, self, field).ch
+ struct_ = self._cached(ch, target_length)
context[CTX_STREAM].write(struct_.pack(*seq))
@override
- def unpack_single(self, context: _ContextLike):
+ def unpack_single(self, context: ContextLike):
"""
Unpack a single value from the stream.
:param context: The context that provides the stream and field-specific information.
:return: The unpacked value, converted to the field's corresponding Python type.
"""
- field = context.get(CTX_FIELD)
- order_ch = (
- field.order.ch
- if field
- else (self.__byteorder__ or O_DEFAULT_ENDIAN.value or LittleEndian).ch
- )
+ order_ch = resolve_order(context, self).ch
struct_ = self._cached(order_ch)
size = struct_.size
data = context[CTX_STREAM].read(size)
@@ -242,7 +240,7 @@ def unpack_single(self, context: _ContextLike):
return value
@override
- def unpack_seq(self, context: _ContextLike) -> Collection[_IT]:
+ def unpack_seq(self, context: ContextLike) -> Collection[_IT]:
"""
Unpack a sequence of values from the stream.
@@ -272,7 +270,8 @@ def unpack_seq(self, context: _ContextLike) -> Collection[_IT]:
if length is Ellipsis:
return super().unpack_seq(context)
- struct_ = self._cached(field.order.ch, length)
+ order_ch = resolve_order(context, self, field).ch
+ struct_ = self._cached(order_ch, length)
size = struct_.size
data = context[CTX_STREAM].read(size)
if len(data) != size:
@@ -514,14 +513,14 @@ def __init__(self, fill: Buffer | int = 0x00, strict: bool = False) -> None:
if not self.fill:
raise ValueError("fill pattern must be at least one byte")
- def __size__(self, context: _ContextLike) -> int:
+ def __size__(self, context: ContextLike) -> int:
"""
Return the size of the padding pattern in bytes.
:param context: A context object that may be used by the field
to determine its size. The padding field ignores the context
and simply returns the length of its ``fill`` attribute.
- :type context: _ContextLike
+ :type context: ContextLike
:return: The length of the padding pattern.
:rtype: int
"""
@@ -536,7 +535,7 @@ def __type__(self) -> type:
"""
return NoneType
- def __unpack__(self, context: _ContextLike) -> None:
+ def __unpack__(self, context: ContextLike) -> None:
"""
Consume the padding from the stream and validate it.
@@ -548,7 +547,7 @@ def __unpack__(self, context: _ContextLike) -> None:
:param context: Context containing the stream and optional field
descriptor. The stream is accessed via ``context[CTX_STREAM]``,
and the optional parent field via ``context.get(CTX_FIELD)``.
- :type context: _ContextLike
+ :type context: ContextLike
:raises ValidationError: If ``strict`` is ``True`` and the
length of the data is not a multiple of the padding length
or the data does not match the pattern.
@@ -587,7 +586,7 @@ def __unpack__(self, context: _ContextLike) -> None:
context,
)
- def __pack__(self, obj: None, context: _ContextLike) -> None:
+ def __pack__(self, obj: None, context: ContextLike) -> None:
"""
Write the padding pattern to the stream.
@@ -602,7 +601,7 @@ def __pack__(self, obj: None, context: _ContextLike) -> None:
:param context: Context containing the stream and optional field
descriptor. The stream is accessed via ``context[CTX_STREAM]``,
and the optional parent field via ``context.get(CTX_FIELD)``.
- :type context: _ContextLike
+ :type context: ContextLike
:raises TypeError: If the field length is provided but is not an
integer.
"""
@@ -657,7 +656,7 @@ class Transformer(
def __init__(self, struct: _StructLike[_IT_transformed, _OT_transformed]) -> None:
self.struct: _StructLike[_IT_transformed, _OT_transformed] = struct
- self.__bits__: _ContextLambda[int] | int | None = getattr(
+ self.__bits__: ContextLambda[int] | int | None = getattr(
self.struct, "__bits__", None
)
@@ -669,7 +668,7 @@ def __type__(self) -> type | str | None:
"""
return self.struct.__type__()
- def __size__(self, context: _ContextLike) -> int:
+ def __size__(self, context: ContextLike) -> int:
"""
Get the size of the data encoded/decoded by the transformer.
@@ -681,7 +680,7 @@ def __size__(self, context: _ContextLike) -> int:
def encode(
self,
obj: _IT,
- context: _ContextLike, # pyright: ignore[reportUnusedParameter]
+ context: ContextLike, # pyright: ignore[reportUnusedParameter]
) -> _IT_transformed:
"""
Encode data using the wrapped _StructLike object.
@@ -695,7 +694,7 @@ def encode(
def decode(
self,
parsed: _OT_transformed,
- context: _ContextLike, # pyright: ignore[reportUnusedParameter]
+ context: ContextLike, # pyright: ignore[reportUnusedParameter]
) -> _OT:
"""
Decode data using the wrapped _StructLike object.
@@ -707,7 +706,7 @@ def decode(
return parsed # pyright: ignore[reportReturnType]
@override
- def pack_single(self, obj: _IT, context: _ContextLike) -> None:
+ def pack_single(self, obj: _IT, context: ContextLike) -> None:
"""
Pack a single value into the stream using encoding.
@@ -718,7 +717,7 @@ def pack_single(self, obj: _IT, context: _ContextLike) -> None:
self.struct.__pack__(value, context)
@override
- def unpack_single(self, context: _ContextLike) -> _OT:
+ def unpack_single(self, context: ContextLike) -> _OT:
"""
Unpack a single value from the stream and decode it.
@@ -761,7 +760,7 @@ def __init__(self, value: _IT, struct: _StructLike[_IT, _IT]) -> None:
self.value: _IT = value
@override
- def encode(self, obj: None, context: _ContextLike) -> _IT:
+ def encode(self, obj: None, context: ContextLike) -> _IT:
"""
Encode data using the constant value. This method will always return
the constant value, regardless of the input. Therefore, :code:`None`
@@ -780,7 +779,7 @@ def encode(self, obj: None, context: _ContextLike) -> _IT:
return self.value
@override
- def decode(self, parsed: _IT, context: _ContextLike) -> _IT:
+ def decode(self, parsed: _IT, context: ContextLike) -> _IT:
"""
Decode data and ensure it matches the constant value. If the
parsed value doesn't match, a `ValidationError` is raised.
@@ -870,7 +869,7 @@ def __type__(self) -> type[_EnumT] | type[_IT]:
return self.model | self.struct.__type__() # pyright: ignore[reportReturnType]
@override
- def encode(self, obj: _EnumT, context: _ContextLike) -> _IT:
+ def encode(self, obj: _EnumT, context: ContextLike) -> _IT:
"""
Encode an enumeration value into its corresponding encoded representation.
@@ -892,7 +891,7 @@ def encode(self, obj: _EnumT, context: _ContextLike) -> _IT:
return obj.value
@override
- def decode(self, parsed: _IT, context: _ContextLike) -> _EnumT:
+ def decode(self, parsed: _IT, context: ContextLike) -> _EnumT:
"""
Decode an encoded value (typically an integer) back to its corresponding
enumeration value.
@@ -996,8 +995,8 @@ class Memory(Generic[_MemoryIT, _MemoryOT], FieldStruct[_MemoryIT, _MemoryOT]):
__slots__: tuple[str, ...] = ("length", "_length_is_lambda")
- def __init__(self, length: _ContextLambda[int] | int | _GreedyType) -> None:
- self.length: _ContextLambda[int] | int | _GreedyType = length
+ def __init__(self, length: ContextLambda[int] | int | _GreedyType) -> None:
+ self.length: ContextLambda[int] | int | _GreedyType = length
self._length_is_lambda: bool = callable(length)
def __type__(self) -> type[memoryview]:
@@ -1008,7 +1007,7 @@ def __type__(self) -> type[memoryview]:
"""
return memoryview
- def __size__(self, context: _ContextLike) -> int: # actually int | _GreedyType
+ def __size__(self, context: ContextLike) -> int: # actually int | _GreedyType
"""
Calculate the size of the memory field based on the `length` parameter.
@@ -1021,7 +1020,7 @@ def __size__(self, context: _ContextLike) -> int: # actually int | _GreedyType
return self.length(context) if self._length_is_lambda else self.length
@override
- def pack_single(self, obj: _MemoryIT, context: _ContextLike) -> None:
+ def pack_single(self, obj: _MemoryIT, context: ContextLike) -> None:
"""
Pack a single byte object (memoryview or bytes) into the stream.
@@ -1057,7 +1056,7 @@ def pack_single(self, obj: _MemoryIT, context: _ContextLike) -> None:
context[CTX_STREAM].write(obj)
@override
- def unpack_single(self, context: _ContextLike) -> _MemoryOT:
+ def unpack_single(self, context: ContextLike) -> _MemoryOT:
"""
Unpack a single byte object (memoryview) from the stream.
@@ -1099,7 +1098,7 @@ def __type__(self) -> type:
return bytes
@override
- def unpack_single(self, context: _ContextLike) -> bytes:
+ def unpack_single(self, context: ContextLike) -> bytes:
"""
Unpack a single byte sequence (bytes) from the stream.
@@ -1130,11 +1129,11 @@ class String(Memory[str, str]):
def __init__(
self,
- length: int | _ContextLambda[int] | _GreedyType,
- encoding: str | _ContextLambda[str] | None = None,
+ length: int | ContextLambda[int] | _GreedyType,
+ encoding: str | ContextLambda[str] | None = None,
) -> None:
super().__init__(length)
- self.encoding: str | _ContextLambda[str] = encoding or "utf-8"
+ self.encoding: str | ContextLambda[str] = encoding or "utf-8"
self._encoding_is_lambda: bool = callable(self.encoding)
@override
@@ -1147,14 +1146,14 @@ def __type__(self) -> type:
return str
@override
- def pack_single(self, obj: str, context: _ContextLike) -> None:
+ def pack_single(self, obj: str, context: ContextLike) -> None:
"""Packs a single string into the stream."""
# fmt: off
encoding: str = self.encoding if not self._encoding_is_lambda else self.encoding(context) # pyright: ignore[reportAssignmentType, reportCallIssue]
return super().pack_single(obj.encode(encoding), context) # pyright: ignore[reportArgumentType]
@override
- def unpack_single(self, context: _ContextLike) -> str:
+ def unpack_single(self, context: ContextLike) -> str:
"""
Unpack a single string from the stream.
@@ -1206,8 +1205,8 @@ class CString(FieldStruct[str, str]):
def __init__(
self,
- length: int | _ContextLambda[int] | _GreedyType | None = None,
- encoding: str | _ContextLambda[str] | None = None,
+ length: int | ContextLambda[int] | _GreedyType | None = None,
+ encoding: str | ContextLambda[str] | None = None,
pad: int | str | None = None,
) -> None:
"""
@@ -1216,10 +1215,10 @@ def __init__(
:param length: The fixed length or a context lambda to determine the length dynamically.
:param encoding: The encoding to use for string encoding/decoding (default is UTF-8).
"""
- self.length: int | _ContextLambda[int] | _GreedyType = (
+ self.length: int | ContextLambda[int] | _GreedyType = (
... if length is None else length
)
- self.encoding: str | _ContextLambda[str] = encoding or "utf-8"
+ self.encoding: str | ContextLambda[str] = encoding or "utf-8"
self.pad: int = 0
self._encoding_is_lambda: bool = callable(self.encoding)
self._length_is_lambda: bool = callable(self.length)
@@ -1243,7 +1242,7 @@ def __class_getitem__(cls, dim: _LengthT) -> Field:
"""
return CString(...)[dim]
- def __size__(self, context: _ContextLike) -> int:
+ def __size__(self, context: ContextLike) -> int:
"""
Returns the size of the `CString` field.
@@ -1262,7 +1261,7 @@ def __type__(self) -> type:
return str
@override
- def pack_single(self, obj: str, context: _ContextLike) -> None:
+ def pack_single(self, obj: str, context: ContextLike) -> None:
"""
Pack a single string into the stream with padding.
@@ -1294,7 +1293,7 @@ def pack_single(self, obj: str, context: _ContextLike) -> None:
stream.write(self._raw_pad)
@override
- def unpack_single(self, context: _ContextLike) -> str:
+ def unpack_single(self, context: ContextLike) -> str:
"""
Unpack a single C-style string from the stream.
@@ -1378,7 +1377,7 @@ class ConstString(Const[str]):
__slots__: tuple[()] = ()
def __init__(
- self, value: str, encoding: str | _ContextLambda[str] | None = None
+ self, value: str, encoding: str | ContextLambda[str] | None = None
) -> None:
if not isinstance(value, str):
raise TypeError("value must be a string")
@@ -1460,8 +1459,8 @@ class Computed(Generic[_IT], FieldStruct[NoneType, _IT]):
__slots__: tuple[str, ...] = ("value",)
- def __init__(self, value: _IT | _ContextLambda[_IT]) -> None:
- self.value: _IT | _ContextLambda[_IT] = value
+ def __init__(self, value: _IT | ContextLambda[_IT]) -> None:
+ self.value: _IT | ContextLambda[_IT] = value
self.__bits__: int = 0
def __type__(self) -> type:
@@ -1473,7 +1472,7 @@ def __type__(self) -> type:
return object if callable(self.value) else type(self.value)
@override
- def __pack__(self, obj: NoneType, context: _ContextLike) -> None:
+ def __pack__(self, obj: NoneType, context: ContextLike) -> None:
"""
No packing is needed for computed fields, as the value is computed dynamically.
@@ -1485,7 +1484,7 @@ def __pack__(self, obj: NoneType, context: _ContextLike) -> None:
"""
pass
- def __size__(self, context: _ContextLike) -> int:
+ def __size__(self, context: ContextLike) -> int:
"""
Return the size of the computed field.
@@ -1497,7 +1496,7 @@ def __size__(self, context: _ContextLike) -> int:
return 0
@override
- def __unpack__(self, context: _ContextLike) -> _IT:
+ def __unpack__(self, context: ContextLike) -> _IT:
"""
Unpack the computed value based on the context.
@@ -1511,7 +1510,7 @@ def __unpack__(self, context: _ContextLike) -> _IT:
return self.value(context) if callable(self.value) else self.value # pyright: ignore[reportReturnType]
@override
- def pack_single(self, obj: NoneType, context: _ContextLike) -> None:
+ def pack_single(self, obj: NoneType, context: ContextLike) -> None:
"""
No packing is needed for computed fields.
@@ -1522,7 +1521,7 @@ def pack_single(self, obj: NoneType, context: _ContextLike) -> None:
pass
@override
- def unpack_single(self, context: _ContextLike) -> _IT:
+ def unpack_single(self, context: ContextLike) -> _IT:
"""
No unpacking is needed for computed fields.
@@ -1583,22 +1582,22 @@ def __type__(self) -> type:
return None.__class__
@override
- def __pack__(self, obj: None, context: _ContextLike) -> None:
+ def __pack__(self, obj: None, context: ContextLike) -> None:
pass
- def __size__(self, context: _ContextLike) -> int:
+ def __size__(self, context: ContextLike) -> int:
return 0
@override
- def __unpack__(self, context: _ContextLike) -> None:
+ def __unpack__(self, context: ContextLike) -> None:
pass
@override
- def pack_single(self, obj: None, context: _ContextLike) -> None:
+ def pack_single(self, obj: None, context: ContextLike) -> None:
pass
@override
- def unpack_single(self, context: _ContextLike) -> None:
+ def unpack_single(self, context: ContextLike) -> None:
pass
@@ -1646,7 +1645,9 @@ def __init__(
encoding: str | None = None,
):
self.prefix: _StructLike[int, int] = prefix
- self.struct: _StructLike[_PrefixIOT, _PrefixIOT] | None = getstruct(struct, struct)
+ self.struct: _StructLike[_PrefixIOT, _PrefixIOT] | None = getstruct(
+ struct, struct
+ )
self.encoding: str | None = encoding
# Support str as second argument
if isinstance(struct, str):
@@ -1667,7 +1668,7 @@ def __type__(self) -> type | str | None:
"""
return bytes if self.struct is None else self.struct.__type__()
- def __size__(self, context: _ContextLike) -> int:
+ def __size__(self, context: ContextLike) -> int:
"""
Prefixed fields do not have a fixed size.
@@ -1679,7 +1680,7 @@ def __size__(self, context: _ContextLike) -> int:
raise DynamicSizeError("Prefixed does not store a size", context)
@override
- def pack_single(self, obj: _PrefixIOT, context: _ContextLike) -> None:
+ def pack_single(self, obj: _PrefixIOT, context: ContextLike) -> None:
"""
Pack a single object into the stream, with the prefix indicating the size.
@@ -1688,11 +1689,16 @@ def pack_single(self, obj: _PrefixIOT, context: _ContextLike) -> None:
:param obj: The object to pack (should be a byte sequence).
:param context: The current context.
"""
- # fmt: off
+ outer_field = context.get(CTX_FIELD)
+ outer_seq = context[CTX_SEQ]
if self.struct is not None:
data = BytesIO()
- with WithoutContextVar(context, CTX_STREAM, data):
- self.struct.__pack__(obj, context)
+ try:
+ with WithoutContextVar(context, CTX_STREAM, data):
+ self.struct.__pack__(obj, context)
+ finally:
+ context[CTX_FIELD] = outer_field
+ context[CTX_SEQ] = outer_seq
context[CTX_SEQ] = False
obj = data.getvalue() # pyright: ignore[reportAssignmentType]
@@ -1701,11 +1707,20 @@ def pack_single(self, obj: _PrefixIOT, context: _ContextLike) -> None:
# fmt: off
obj = obj.encode(self.encoding) # pyright: ignore[reportAttributeAccessIssue, reportUnknownMemberType]
- self.prefix.__pack__(len(obj), context) # pyright: ignore[reportArgumentType]
+ if type(self.prefix) is PyStructFormattedField:
+ self.prefix.pack_single(len(obj), context)
+ else:
+ try:
+ self.prefix.__pack__(
+ len(obj), context
+ ) # pyright: ignore[reportArgumentType]
+ finally:
+ context[CTX_FIELD] = outer_field
+ context[CTX_SEQ] = outer_seq
context[CTX_STREAM].write(obj)
@override
- def unpack_single(self, context: _ContextLike) -> _PrefixIOT:
+ def unpack_single(self, context: ContextLike) -> _PrefixIOT:
"""
Unpack a single object from the stream, using the prefix to determine the size.
@@ -1716,7 +1731,16 @@ def unpack_single(self, context: _ContextLike) -> _PrefixIOT:
:param context: The current context.
:return: The unpacked object, which is either raw bytes or the data structure.
"""
- size = self.prefix.__unpack__(context)
+ outer_field = context.get(CTX_FIELD)
+ outer_seq = context[CTX_SEQ]
+ if type(self.prefix) is PyStructFormattedField:
+ size = self.prefix.unpack_single(context)
+ else:
+ try:
+ size = self.prefix.__unpack__(context)
+ finally:
+ context[CTX_FIELD] = outer_field
+ context[CTX_SEQ] = outer_seq
data = context[CTX_STREAM].read(size)
if len(data) != size:
raise ValidationError(
@@ -1727,11 +1751,15 @@ def unpack_single(self, context: _ContextLike) -> _PrefixIOT:
obj = data
if self.struct is not None:
inner = BytesIO(data)
- with (
- WithoutContextVar(context, CTX_STREAM, inner),
- WithoutContextVar(context, CTX_SEQ, False),
- ):
- obj = self.struct.__unpack__(context)
+ try:
+ with (
+ WithoutContextVar(context, CTX_STREAM, inner),
+ WithoutContextVar(context, CTX_SEQ, False),
+ ):
+ obj = self.struct.__unpack__(context)
+ finally:
+ context[CTX_FIELD] = outer_field
+ context[CTX_SEQ] = outer_seq
if inner.tell() != size:
raise ValidationError(
f"Prefixed inner struct consumed {inner.tell()} of {size} bytes",
@@ -1778,7 +1806,7 @@ def __init__(self, bits: int, signed: bool = True) -> None:
else:
self.min_value = 0
self.max_value = self.mask
- self.__byteorder__: _EndianLike | None = None
+ self.__byteorder__: EndianLike | None = None
@override
def __repr__(self) -> str:
@@ -1796,7 +1824,7 @@ def __type__(self) -> type:
"""
return int
- def __size__(self, context: _ContextLike) -> int:
+ def __size__(self, context: ContextLike) -> int:
"""
Return the size of the integer in bytes.
@@ -1806,7 +1834,7 @@ def __size__(self, context: _ContextLike) -> int:
return self.size
@override
- def pack_single(self, obj: int, context: _ContextLike) -> None:
+ def pack_single(self, obj: int, context: ContextLike) -> None:
"""
Pack a single integer value into the stream.
@@ -1818,12 +1846,7 @@ def pack_single(self, obj: int, context: _ContextLike) -> None:
:param context: The current context, which provides the byte order (little-endian or big-endian).
:raises ValueError: If the integer is too large or small for the given bit width.
"""
- field = context.get(CTX_FIELD)
- is_little = (
- field.order.ch
- if field
- else (self.__byteorder__ or O_DEFAULT_ENDIAN.value or LittleEndian).ch
- ) == LITTLE_ENDIAN_FMT
+ is_little = resolve_order(context, self).ch == LITTLE_ENDIAN_FMT
if obj < self.min_value or obj > self.max_value:
raise OverflowError(
f"int too big to convert: {obj!r} does not fit in {self.__bits__} bits"
@@ -1838,7 +1861,7 @@ def pack_single(self, obj: int, context: _ContextLike) -> None:
)
@override
- def unpack_single(self, context: _ContextLike) -> int:
+ def unpack_single(self, context: ContextLike) -> int:
"""
Unpack a single integer value from the stream.
@@ -1849,13 +1872,7 @@ def unpack_single(self, context: _ContextLike) -> int:
:return: The unpacked integer value.
:raises ValueError: If the data cannot be unpacked as an integer of the specified size.
"""
- field: Field = context.get(CTX_FIELD)
- is_little = (
- field.order.ch
- if field
- else (self.__byteorder__ or O_DEFAULT_ENDIAN.value or LittleEndian).ch
- ) == LITTLE_ENDIAN_FMT
-
+ is_little = resolve_order(context, self).ch == LITTLE_ENDIAN_FMT
value = int.from_bytes(
read_exact(context, self.size, f"{type(self).__name__}{self.__bits__}"),
"little" if is_little else "big",
@@ -1924,7 +1941,7 @@ class Aligned(FieldStruct[_IT, _OT]):
def __init__(
self,
struct: _StructLike[_IT, _OT],
- alignment: int | _ContextLambda[int],
+ alignment: int | ContextLambda[int],
after: bool = False,
before: bool = False,
filler: int | str | None = None,
@@ -1933,7 +1950,7 @@ def __init__(
raise ValueError("Must specify either before or after")
self.struct: _StructLike[_IT, _OT] = struct
- self.alignment: int | _ContextLambda[int] = alignment
+ self.alignment: int | ContextLambda[int] = alignment
if not callable(alignment):
_validate_alignment(alignment)
self._after: bool = after
@@ -1957,7 +1974,7 @@ def __type__(self):
"""
return self.struct.__type__()
- def __size__(self, context: _ContextLike) -> int:
+ def __size__(self, context: ContextLike) -> int:
"""
Calculate the size of the aligned field, accounting for padding based on the alignment.
@@ -1974,7 +1991,7 @@ def __size__(self, context: _ContextLike) -> int:
_validate_alignment(self.alignment)
return struct_size + _align_padding(struct_size, self.alignment)
- def unpack_alignment(self, context: _ContextLike) -> None:
+ def unpack_alignment(self, context: ContextLike) -> None:
"""
Unpack padding for the alignment, verifying that the correct amount of padding is present.
@@ -1994,7 +2011,7 @@ def unpack_alignment(self, context: _ContextLike) -> None:
)
@override
- def unpack_single(self, context: _ContextLike) -> _OT:
+ def unpack_single(self, context: ContextLike) -> _OT:
"""
Unpack a single aligned field from the stream.
@@ -2011,7 +2028,7 @@ def unpack_single(self, context: _ContextLike) -> _OT:
self.unpack_alignment(context)
return obj
- def pack_alignment(self, context: _ContextLike) -> None:
+ def pack_alignment(self, context: ContextLike) -> None:
"""
Apply padding for the alignment before or after the structure, depending on
the `before` and `after` settings.
@@ -2025,7 +2042,7 @@ def pack_alignment(self, context: _ContextLike) -> None:
stream.write(bytes([self._filler] * size))
@override
- def pack_single(self, obj: _IT, context: _ContextLike) -> None:
+ def pack_single(self, obj: _IT, context: ContextLike) -> None:
"""
Pack a single aligned field into the stream, applying padding if necessary.
@@ -2039,7 +2056,7 @@ def pack_single(self, obj: _IT, context: _ContextLike) -> None:
self.pack_alignment(context)
-def align(alignment: int | _ContextLambda[int]) -> _ContextLambda[int]:
+def align(alignment: int | ContextLambda[int]) -> ContextLambda[int]:
"""
Create a context lambda to calculate the alignment padding required at the current stream position.
@@ -2064,7 +2081,7 @@ def align(alignment: int | _ContextLambda[int]) -> _ContextLambda[int]:
:return: A context lambda function that returns the number of bytes to align the next structure.
"""
- def _get_aligned_size(context: _ContextLike):
+ def _get_aligned_size(context: ContextLike):
pos = context[CTX_STREAM].tell()
value: int = alignment(context) if callable(alignment) else alignment
_validate_alignment(value)
@@ -2150,7 +2167,7 @@ def __type__(self) -> type | str | None:
"""
return self.struct.__type__()
- def __size__(self, context: _ContextLike) -> int:
+ def __size__(self, context: ContextLike) -> int:
"""
Get the size of the Lazy struct by delegating to the underlying struct.
@@ -2161,7 +2178,7 @@ def __size__(self, context: _ContextLike) -> int:
return self.struct.__size__(context)
@override
- def pack_single(self, obj: _IT, context: _ContextLike) -> None:
+ def pack_single(self, obj: _IT, context: ContextLike) -> None:
"""
Pack a single value using the Lazy struct by delegating to the underlying struct.
@@ -2171,7 +2188,7 @@ def pack_single(self, obj: _IT, context: _ContextLike) -> None:
self.struct.__pack__(obj, context)
@override
- def unpack_single(self, context: _ContextLike) -> _OT:
+ def unpack_single(self, context: ContextLike) -> _OT:
"""
Unpack a single value using the Lazy struct by delegating to the underlying struct.
@@ -2213,7 +2230,7 @@ def __type__(self) -> type:
"""
return UUID
- def __size__(self, context: _ContextLike) -> int:
+ def __size__(self, context: ContextLike) -> int:
"""
Get the size of the UUID field.
@@ -2233,7 +2250,7 @@ def __bits__(self) -> int:
return 128
@override
- def __pack__(self, obj: UUID, context: _ContextLike) -> None:
+ def __pack__(self, obj: UUID, context: ContextLike) -> None:
"""
Pack a UUID object into the stream.
@@ -2245,13 +2262,12 @@ def __pack__(self, obj: UUID, context: _ContextLike) -> None:
"""
if not isinstance(obj, UUID):
obj = UUID(obj)
- field = context.get(CTX_FIELD)
- order = field.order if field else (O_DEFAULT_ENDIAN.value or LittleEndian)
- is_le = order.ch == LITTLE_ENDIAN_FMT
+
+ is_le = resolve_order(context, self).ch == LITTLE_ENDIAN_FMT
context[CTX_STREAM].write(obj.bytes_le if is_le else obj.bytes)
@override
- def __unpack__(self, context: _ContextLike) -> UUID:
+ def __unpack__(self, context: ContextLike) -> UUID:
"""
Unpack a UUID from the stream.
@@ -2262,10 +2278,8 @@ def __unpack__(self, context: _ContextLike) -> UUID:
:return: The unpacked `UUID` object.
:rtype: UUID
"""
- field = context.get(CTX_FIELD)
- order = field.order if field else (O_DEFAULT_ENDIAN.value or LittleEndian)
- is_le = order.ch == LITTLE_ENDIAN_FMT
- data = context[CTX_STREAM].read(16)
+ is_le = resolve_order(context, self).ch == LITTLE_ENDIAN_FMT
+ data = read_exact(context, 16, "UUID")
return UUID(bytes_le=data) if is_le else UUID(bytes=data)
@@ -2314,7 +2328,7 @@ def __rmod__(self, other: _StructLike[int, int]) -> Self:
def __type__(self) -> type:
return int
- def __size__(self, context: _ContextLike) -> int:
+ def __size__(self, context: ContextLike) -> int:
if self.struct is None:
return 0
return self.struct.__size__(context)
@@ -2324,7 +2338,7 @@ def __bits__(self) -> int:
return 0
return self.struct.__bits__() # pyright: ignore[reportAttributeAccessIssue]
- def _target_field(self, context: _ContextLike) -> Field | None:
+ def _target_field(self, context: ContextLike) -> Field | None:
*parts, target_name = self.target.removeprefix("_obj.").split(".")
obj = context.__context_getattr__(".".join(["_obj"] + list(parts)))
struct = getstruct(obj, None)
@@ -2333,7 +2347,7 @@ def _target_field(self, context: _ContextLike) -> Field | None:
return None
return members.get(target_name)
- def _target_length(self, target_obj: Any, context: _ContextLike) -> int:
+ def _target_length(self, target_obj: Any, context: ContextLike) -> int:
if isinstance(target_obj, str):
field = self._target_field(context)
target_struct = getattr(field, "struct", None) if field else None
@@ -2345,7 +2359,7 @@ def _target_length(self, target_obj: Any, context: _ContextLike) -> int:
return len(target_obj.encode(encoding))
return len(target_obj)
- def __pack__(self, obj: None, context: _ContextLike):
+ def __pack__(self, obj: None, context: ContextLike):
# object is optional
if self.struct is None:
raise ValueError("struct is not defined")
@@ -2355,7 +2369,7 @@ def __pack__(self, obj: None, context: _ContextLike):
context.__context_setattr__(self.name, length)
self.struct.__pack__(length, context)
- def __unpack__(self, context: _ContextLike) -> int:
+ def __unpack__(self, context: ContextLike) -> int:
if self.struct is None:
raise ValueError("struct is not defined")
@@ -2424,7 +2438,7 @@ def __init__(
pass # silently ignore
@override
- def encode(self, obj: datetime.datetime, context: _ContextLike) -> _TimestampT:
+ def encode(self, obj: datetime.datetime, context: ContextLike) -> _TimestampT:
"""Encode a ``datetime.datetime`` object into a Unix timestamp.
The resulting timestamp is returned as either a floating-point or
@@ -2433,7 +2447,7 @@ def encode(self, obj: datetime.datetime, context: _ContextLike) -> _TimestampT:
:param obj: Datetime object to be encoded
:type obj: datetime.datetime
:param context: Transformation context used during encoding
- :type context: _ContextLike
+ :type context: ContextLike
:return: Unix timestamp representation of the datetime
:rtype: _TimestampT
"""
@@ -2444,7 +2458,7 @@ def encode(self, obj: datetime.datetime, context: _ContextLike) -> _TimestampT:
return ts if self.floating_point else int(ts) # pyright: ignore[reportReturnType]
@override
- def decode(self, parsed: _TimestampT, context: _ContextLike) -> datetime.datetime:
+ def decode(self, parsed: _TimestampT, context: ContextLike) -> datetime.datetime:
"""Decode a Unix timestamp into a ``datetime.datetime`` object.
The timestamp is converted to a float before constructing the datetime
@@ -2453,7 +2467,7 @@ def decode(self, parsed: _TimestampT, context: _ContextLike) -> datetime.datetim
:param parsed: Parsed timestamp value
:type parsed: _TimestampT
:param context: Transformation context used during decoding
- :type context: _ContextLike
+ :type context: ContextLike
:return: Reconstructed datetime object
:rtype: datetime.datetime
"""
@@ -2502,16 +2516,16 @@ def __init__(
self,
struct: _StructLike[_IT, _OT] | type,
*,
- before: int | _ContextLambda[int] = 0,
- after: int | _ContextLambda[int] = 0,
+ before: int | ContextLambda[int] = 0,
+ after: int | ContextLambda[int] = 0,
fill: Buffer | int = 0x00,
strict: bool = False,
) -> None:
self.struct: _StructLike[_IT, _OT] = (
getstruct(struct) or struct
) # pyright: ignore[reportAttributeAccessIssue]
- self.before: int | _ContextLambda[int] = before
- self.after: int | _ContextLambda[int] = after
+ self.before: int | ContextLambda[int] = before
+ self.after: int | ContextLambda[int] = after
fill_bytes = _normalize_fill(fill)
self._before_fill: bytes = fill_bytes
self._after_fill: bytes = fill_bytes
@@ -2523,8 +2537,8 @@ def new(
cls,
struct: _StructLike[_IT, _OT] | type,
*,
- before: int | _ContextLambda[int] = 0,
- after: int | _ContextLambda[int] = 0,
+ before: int | ContextLambda[int] = 0,
+ after: int | ContextLambda[int] = 0,
before_fill: bytes,
after_fill: bytes,
before_strict: bool,
@@ -2540,7 +2554,7 @@ def new(
def __type__(self) -> type | str | None:
return self.struct.__type__()
- def __size__(self, context: _ContextLike) -> int:
+ def __size__(self, context: ContextLike) -> int:
if callable(self.before) or callable(self.after):
raise DynamicSizeError(
"Padded fields with dynamic padding don't have a fixed size"
@@ -2555,7 +2569,7 @@ def _fill_bytes(self, fill: bytes, length: int) -> bytes:
return (fill * ((length + len(fill) - 1) // len(fill)))[:length]
def _resolve_length(
- self, length: int | _ContextLambda[int], context: _ContextLike
+ self, length: int | ContextLambda[int], context: ContextLike
) -> int:
value = length(context) if callable(length) else length
if not isinstance(value, int):
@@ -2567,7 +2581,7 @@ def _resolve_length(
return value
def _read_padding(
- self, length: int, fill: bytes, strict: bool, context: _ContextLike
+ self, length: int, fill: bytes, strict: bool, context: ContextLike
) -> None:
data = read_exact(context, length, "Padded")
expected = self._fill_bytes(fill, length)
@@ -2579,11 +2593,11 @@ def _read_padding(
context,
)
- def _write_padding(self, length: int, fill: bytes, context: _ContextLike) -> None:
+ def _write_padding(self, length: int, fill: bytes, context: ContextLike) -> None:
context[CTX_STREAM].write(self._fill_bytes(fill, length))
@override
- def unpack_single(self, context: _ContextLike) -> _OT:
+ def unpack_single(self, context: ContextLike) -> _OT:
before = self._resolve_length(self.before, context)
self._read_padding(before, self._before_fill, self._before_strict, context)
obj = self.struct.__unpack__(context)
@@ -2592,7 +2606,7 @@ def unpack_single(self, context: _ContextLike) -> _OT:
return obj
@override
- def pack_single(self, obj: _IT, context: _ContextLike) -> None:
+ def pack_single(self, obj: _IT, context: ContextLike) -> None:
before = self._resolve_length(self.before, context)
self._write_padding(before, self._before_fill, context)
self.struct.__pack__(obj, context)
@@ -2606,13 +2620,13 @@ class _PadSpec:
def __init__(
self,
side: str,
- length: int | _ContextLambda[int],
+ length: int | ContextLambda[int],
*,
fill: Buffer | int = 0x00,
strict: bool = False,
) -> None:
self.side: str = side
- self.length: int | _ContextLambda[int] = length
+ self.length: int | ContextLambda[int] = length
self.fill: bytes = _normalize_fill(fill)
self.strict: bool = strict
@@ -2665,7 +2679,7 @@ def __rtruediv__(self, struct: _StructLike[_IT, _OT] | type) -> Padded[_IT, _OT]
def PrePad(
- length: int | _ContextLambda[int],
+ length: int | ContextLambda[int],
*,
fill: Buffer | int = 0x00,
strict: bool = False,
@@ -2680,7 +2694,7 @@ def PrePad(
def PostPad(
- length: int | _ContextLambda[int],
+ length: int | ContextLambda[int],
*,
fill: Buffer | int = 0x00,
strict: bool = False,
diff --git a/src/caterpillar/fields/compression.py b/src/caterpillar/fields/compression.py
old mode 100755
new mode 100644
index 9c1ec17a..40fca3b4
--- a/src/caterpillar/fields/compression.py
+++ b/src/caterpillar/fields/compression.py
@@ -14,17 +14,13 @@
# along with this program. If not, see .
# pyright: reportPrivateUsage=false, reportAny=false, reportExplicitAny=false
from typing import Any, Protocol, runtime_checkable
+
from typing_extensions import override
-from caterpillar.abc import (
- _ContainsStruct,
- _ContextLike,
- _StructLike,
- _LengthT,
-)
-from caterpillar.shared import getstruct, hasstruct
+from caterpillar.abc import _ContainsStruct, _ContextLike, _LengthT, _StructLike
from caterpillar.fields._mixin import get_kwargs
-from caterpillar.fields.common import Transformer, Bytes
+from caterpillar.fields.common import Bytes, Transformer
+from caterpillar.shared import getstruct, hasstruct
@runtime_checkable
diff --git a/src/caterpillar/fields/conditional.py b/src/caterpillar/fields/conditional.py
old mode 100755
new mode 100644
index 3de21686..6c3ae00c
--- a/src/caterpillar/fields/conditional.py
+++ b/src/caterpillar/fields/conditional.py
@@ -16,24 +16,24 @@
# pyright: reportAny=false
import operator
import sys
-
from types import TracebackType
from typing import Annotated, Any, get_args, get_origin
-from typing_extensions import Final, override, Self
-from caterpillar.context import BinaryExpression, ConditionContext, UnaryExpression
-from caterpillar.exception import StructException, ValidationError
-from caterpillar.registry import to_struct
-from caterpillar.shared import iscond, iscondend, iscondstart, typeof, constval
+from typing_extensions import Final, Self, override
+
from caterpillar.abc import (
+ _AnnotationT,
_ArchLike,
_ContextLambda,
_ContextLike,
_EndianLike,
_OptionLike,
_StructLike,
- _AnnotationT,
)
+from caterpillar.context import BinaryExpression, ConditionContext, UnaryExpression
+from caterpillar.exception import StructException, ValidationError
+from caterpillar.registry import to_struct
+from caterpillar.shared import constval, iscond, iscondend, iscondstart, typeof
from ._base import Field
diff --git a/src/caterpillar/fields/crypto.py b/src/caterpillar/fields/crypto.py
old mode 100755
new mode 100644
index 2b47fbfd..3710b48e
--- a/src/caterpillar/fields/crypto.py
+++ b/src/caterpillar/fields/crypto.py
@@ -17,28 +17,28 @@
from collections.abc import Iterable
from typing import TYPE_CHECKING, Any, Protocol, runtime_checkable
+
from typing_extensions import override
-from caterpillar.exception import UnsupportedOperation
-from caterpillar.exception import InvalidValueError
-from caterpillar.context import CTX_STREAM
from caterpillar.abc import (
+ _IT,
+ _ArgType,
_ContextLambda,
_ContextLike,
_GreedyType,
_StructLike,
- _IT,
- _ArgType,
)
+from caterpillar.context import CTX_STREAM
+from caterpillar.exception import InvalidValueError, UnsupportedOperation
-from .common import Memory, Bytes
from ._mixin import get_args, get_kwargs
+from .common import Bytes, Memory
if TYPE_CHECKING:
from cryptography.hazmat.primitives.ciphers import (
- modes,
CipherAlgorithm,
CipherContext,
+ modes,
)
from cryptography.hazmat.primitives.padding import PaddingContext
diff --git a/src/caterpillar/fields/digest.py b/src/caterpillar/fields/digest.py
old mode 100755
new mode 100644
index d2094baf..642ececb
--- a/src/caterpillar/fields/digest.py
+++ b/src/caterpillar/fields/digest.py
@@ -13,19 +13,21 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
# pyright: reportPrivateUsage=false, reportExplicitAny=false
-import sys
import hashlib
-from types import FrameType, TracebackType
-from typing import TYPE_CHECKING, Any, Callable, Generic, Protocol
-from typing_extensions import Literal, Self, override, TypeVar
+import sys
import warnings
import zlib
+from types import FrameType, TracebackType
+from typing import TYPE_CHECKING, Any, Callable, Generic, Protocol
+from typing_extensions import Literal, Self, TypeVar, override
+
+from caterpillar.abc import _ContextLambda, _ContextLike, _StructLike
from caterpillar.context import CTX_OBJECT, CTX_STREAM
from caterpillar.exception import StructException, ValidationError
-from caterpillar.shared import Action
from caterpillar.fields.hook import IOHook
-from caterpillar.abc import _ContextLambda, _ContextLike, _StructLike
+from caterpillar.shared import Action
+
from ._base import Field
from .common import Bytes, uint32
diff --git a/src/caterpillar/fields/hook.py b/src/caterpillar/fields/hook.py
old mode 100755
new mode 100644
index c18e3285..e84cbbb4
--- a/src/caterpillar/fields/hook.py
+++ b/src/caterpillar/fields/hook.py
@@ -15,10 +15,11 @@
# pyright: reportPrivateUsage=false
from io import RawIOBase
from typing import Callable
-from typing_extensions import override, Buffer
+from typing_extensions import Buffer, override
+
+from caterpillar.abc import _ContextLambda, _ContextLike
from caterpillar.context import CTX_STREAM
-from caterpillar.abc import _ContextLike, _ContextLambda
HookInit = _ContextLambda[None]
HookUpdate = Callable[[bytes, _ContextLike], bytes | None]
diff --git a/src/caterpillar/fields/net.py b/src/caterpillar/fields/net.py
old mode 100755
new mode 100644
index 2c8b5158..b2a6b562
--- a/src/caterpillar/fields/net.py
+++ b/src/caterpillar/fields/net.py
@@ -12,21 +12,22 @@
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
-import ipaddress
import binascii
+import ipaddress
import re
-
from typing import Final
+
from typing_extensions import override
from caterpillar.abc import _ContextLike # pyright: ignore[reportPrivateUsage]
-from .common import Transformer, Bytes
+
from ._base import singleton
+from .common import Bytes, Transformer
@singleton
class IPv4Address(
- Transformer[ipaddress.IPv4Address| str | int, bytes, ipaddress.IPv4Address, bytes]
+ Transformer[ipaddress.IPv4Address | str | int, bytes, ipaddress.IPv4Address, bytes]
):
"""
A transformer for encoding and decoding IPv4 addresses.
@@ -51,7 +52,9 @@ def __type__(self) -> type:
return ipaddress.IPv4Address
@override
- def encode(self, obj: ipaddress.IPv4Address| str | int, context: _ContextLike) -> bytes:
+ def encode(
+ self, obj: ipaddress.IPv4Address | str | int, context: _ContextLike
+ ) -> bytes:
"""
Encode an IPv4Address object.
@@ -102,7 +105,9 @@ def __type__(self) -> type:
return ipaddress.IPv6Address
@override
- def encode(self, obj: ipaddress.IPv6Address | str | int, context: _ContextLike) -> bytes:
+ def encode(
+ self, obj: ipaddress.IPv6Address | str | int, context: _ContextLike
+ ) -> bytes:
"""
Encode an IPv6Address object.
diff --git a/src/caterpillar/fields/pointer.py b/src/caterpillar/fields/pointer.py
old mode 100755
new mode 100644
index 0931f849..8de5e068
--- a/src/caterpillar/fields/pointer.py
+++ b/src/caterpillar/fields/pointer.py
@@ -14,20 +14,32 @@
# along with this program. If not, see .
from typing import Any, Final, Generic
-from typing_extensions import override, TypeVar
+from typing_extensions import TypeVar, override
+
+from caterpillar._common import WithoutContextVar
+from caterpillar.abc import _IT, ContextLambda, ContextLike, StructLike, _StreamType
from caterpillar.byteorder import Arch
+from caterpillar.context import CTX_ARCH, CTX_FIELD, CTX_SEQ, CTX_STREAM
from caterpillar.exception import DelegationError, StructException
-from caterpillar.context import CTX_STREAM, CTX_FIELD, CTX_ARCH, CTX_SEQ
from caterpillar.options import Flag
-from caterpillar._common import WithoutContextVar
from caterpillar.shared import getstruct
-from caterpillar.abc import _ContextLambda, _ContextLike, _StructLike, _StreamType, _IT
from ._mixin import FieldStruct
-from .common import uint16, uint24, uint32, uint64, uint8
-from .common import int16, int32, int64, int24, int8
-from .common import UInt, Int
+from .common import (
+ Int,
+ UInt,
+ int8,
+ int16,
+ int24,
+ int32,
+ int64,
+ uint8,
+ uint16,
+ uint24,
+ uint32,
+ uint64,
+)
_PtrValueT = TypeVar("_PtrValueT", default=None)
@@ -35,7 +47,7 @@
PTR_STRICT: Flag[None] = Flag("pointer.strict-mode")
-class pointer(Generic[_PtrValueT], int):
+class pointer(int, Generic[_PtrValueT]):
"""
A custom integer subclass representing a pointer to another struct within the stream.
@@ -54,9 +66,11 @@ def __repr__(self) -> str:
def get(self):
return self.obj
-_PtrT = TypeVar('_PtrT', default=pointer)
-class Pointer(Generic[_PtrT, _PtrValueT], FieldStruct[int, _PtrT]):
+_PtrT = TypeVar("_PtrT", default=pointer)
+
+
+class Pointer(FieldStruct[int, _PtrT], Generic[_PtrT, _PtrValueT]):
"""
A struct that represents a pointer to another struct within the stream.
@@ -68,17 +82,15 @@ class Pointer(Generic[_PtrT, _PtrValueT], FieldStruct[int, _PtrT]):
def __init__(
self,
- struct: _StructLike[int, int] | _ContextLambda[_StructLike[int, int]],
- model: _StructLike[_PtrValueT, _PtrValueT] | type[_PtrValueT] | None = None,
+ struct: StructLike[int, int] | ContextLambda[StructLike[int, int]],
+ model: StructLike[_PtrValueT, _PtrValueT] | type[_PtrValueT] | None = None,
) -> None:
- self.struct: _StructLike[int, int] | _ContextLambda[_StructLike[int, int]] = (
- struct
- )
- self.model: _StructLike[_PtrValueT, _PtrValueT] | None = (
+ self.struct: StructLike[int, int] | ContextLambda[StructLike[int, int]] = struct
+ self.model: StructLike[_PtrValueT, _PtrValueT] | None = (
getstruct(model, model) if model is not None else None
)
- def __mul__(self, model: _StructLike[_IT, _IT]) -> "Pointer[_IT]":
+ def __mul__(self, model: StructLike[_IT, _IT]) -> "Pointer[_IT]":
"""
Create a new Pointer with a specified model.
@@ -97,7 +109,7 @@ def __type__(self) -> type[_PtrT]:
"""
return pointer
- def __size__(self, context: _ContextLike) -> int:
+ def __size__(self, context: ContextLike) -> int:
"""
Get the size of the Pointer struct.
@@ -111,7 +123,7 @@ def __size__(self, context: _ContextLike) -> int:
return struct.__size__(context)
@override
- def unpack_single(self, context: _ContextLike) -> _PtrT:
+ def unpack_single(self, context: ContextLike) -> _PtrT:
"""
Unpack a single value using the Pointer struct.
@@ -125,10 +137,13 @@ def unpack_single(self, context: _ContextLike) -> _PtrT:
stream: _StreamType = context[CTX_STREAM] # pyright: ignore[reportAny]
start = stream.tell()
+ outer_field = context.get(CTX_FIELD)
with WithoutContextVar(context, CTX_SEQ, False):
value: int = struct.__unpack__(context)
# cleanup before further parsing
value: int = self._clean(value, context)
+ context[CTX_FIELD] = outer_field
+ context[CTX_SEQ] = False
if self.model is None:
return self._create(value, start, None, context)
@@ -149,16 +164,16 @@ def unpack_single(self, context: _ContextLike) -> _PtrT:
try:
model_obj = self.model.__unpack__(context)
except StructException as exc:
- field = context.get(CTX_FIELD)
- if field is not None and field.has_flag(PTR_STRICT):
+ if outer_field is not None and outer_field.has_flag(PTR_STRICT):
raise DelegationError(
"Could not parse model!", context
) from exc
- stream.seek(fallback)
+ finally:
+ stream.seek(fallback)
return self._create(value, start, model_obj, context)
@override
- def pack_single(self, obj: int, context: _ContextLike) -> None:
+ def pack_single(self, obj: int, context: ContextLike) -> None:
"""
Pack a single value using the Pointer struct.
@@ -170,6 +185,7 @@ def pack_single(self, obj: int, context: _ContextLike) -> None:
if callable(struct):
struct = self.struct(context) # pyright: ignore[reportCallIssue]
+ outer_field = context.get(CTX_FIELD)
with WithoutContextVar(context, CTX_SEQ, False):
value = int(obj)
model_obj = None
@@ -180,6 +196,8 @@ def pack_single(self, obj: int, context: _ContextLike) -> None:
start = stream.tell()
struct.__pack__(value, context)
+ context[CTX_FIELD] = outer_field
+ context[CTX_SEQ] = False
if model_obj is None:
return
@@ -196,7 +214,7 @@ def pack_single(self, obj: int, context: _ContextLike) -> None:
finally:
stream.seek(fallback)
- def _to_offset(self, value: int, start: int, context: _ContextLike) -> int:
+ def _to_offset(self, value: int, start: int, context: ContextLike) -> int:
"""
Convert the pointer value to an offset.
@@ -208,7 +226,7 @@ def _to_offset(self, value: int, start: int, context: _ContextLike) -> int:
"""
return value
- def _clean(self, value: int, context: _ContextLike) -> int:
+ def _clean(self, value: int, context: ContextLike) -> int:
"""
Clean the pointer value.
@@ -223,7 +241,7 @@ def _create(
value: int,
start: int,
model_obj: _PtrValueT | None,
- context: _ContextLike,
+ context: ContextLike,
) -> pointer[_PtrValueT]:
"""
Create a new pointer object.
@@ -239,22 +257,22 @@ def _create(
return ptr
-UNSIGNED_POINTER_TYS: dict[int, _StructLike[int, int]] = {
+UNSIGNED_POINTER_TYS: dict[int, StructLike[int, int]] = {
x.__bits__: x for x in [uint8, uint16, uint24, uint32, uint64]
}
-SIGNED_POINTER_TYS: dict[int, _StructLike[int, int]] = {
+SIGNED_POINTER_TYS: dict[int, StructLike[int, int]] = {
x.__bits__: x for x in [int8, int16, int24, int32, int64]
}
-def uintptr_fn(context: _ContextLike) -> _StructLike[int, int]:
+def uintptr_fn(context: ContextLike) -> StructLike[int, int]:
"""
Generator function to decide which struct to use as the pointer type based
on the current architecture.
- :param _ContextLike context: The input context.
+ :param ContextLike context: The input context.
:return: The struct to use.
- :rtype: _StructLike
+ :rtype: StructLike
"""
field = context[CTX_FIELD]
arch: Arch = (
@@ -263,14 +281,14 @@ def uintptr_fn(context: _ContextLike) -> _StructLike[int, int]:
return UNSIGNED_POINTER_TYS.get(arch.ptr_size, UInt(arch.ptr_size))
-def intptr_fn(context: _ContextLike) -> _StructLike[int, int]:
+def intptr_fn(context: ContextLike) -> StructLike[int, int]:
"""
Generator function to decide which struct to use as the pointer type based
on the current architecture.
- :param _ContextLike context: The input context.
+ :param ContextLike context: The input context.
:return: The struct to use.
- :rtype: _StructLike
+ :rtype: StructLike
"""
field = context[CTX_FIELD]
arch: Arch = (
@@ -324,7 +342,7 @@ def __type__(self):
return relative_pointer
@override
- def _to_offset(self, value: int, start: int, context: _ContextLike) -> int:
+ def _to_offset(self, value: int, start: int, context: ContextLike) -> int:
"""
Convert the relative pointer value to an offset.
@@ -342,7 +360,7 @@ def _create(
value: int,
start: int,
model_obj: _PtrValueT | None,
- context: _ContextLike,
+ context: ContextLike,
) -> pointer[_PtrValueT]:
"""
Create a new relative pointer object.
diff --git a/src/caterpillar/fields/varint.py b/src/caterpillar/fields/varint.py
old mode 100755
new mode 100644
index bc5cdaf4..e979500c
--- a/src/caterpillar/fields/varint.py
+++ b/src/caterpillar/fields/varint.py
@@ -13,27 +13,19 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
from __future__ import annotations
+
import typing
+
from typing_extensions import override
-from caterpillar.exception import (
- InvalidValueError,
- DynamicSizeError,
- StreamError,
-)
-from caterpillar.byteorder import (
- O_DEFAULT_ENDIAN,
- LittleEndian,
- LITTLE_ENDIAN_FMT,
-)
+from caterpillar.abc import _ContextLike, _EndianLike, _StreamType
+from caterpillar.byteorder import LITTLE_ENDIAN_FMT
from caterpillar.context import CTX_FIELD, CTX_STREAM
+from caterpillar.exception import DynamicSizeError, InvalidValueError, StreamError
from caterpillar.options import Flag
-from caterpillar.abc import _ContextLike, _EndianLike, _StreamType
from ._mixin import FieldStruct
-
-if typing.TYPE_CHECKING:
- from caterpillar.fields import Field
+from .common import resolve_order
VARINT_LSB = Flag("varint.lsb")
@@ -92,13 +84,7 @@ def pack_single(self, obj: int, context: _ContextLike) -> None:
raise InvalidValueError("Invalid negative value for VarInt encoding!")
stream: _StreamType = context[CTX_STREAM]
- field: "Field" = context.get(CTX_FIELD)
- order: _EndianLike = (
- field.order
- if field
- else (self.__byteorder__ or O_DEFAULT_ENDIAN.value or LittleEndian)
- )
- is_little: bool = order.ch == LITTLE_ENDIAN_FMT
+ is_little = resolve_order(context, self).ch == LITTLE_ENDIAN_FMT
hb, lb = self.bit_config(context)
# This implementation is using LittleEndian. Later we can use reverse to
@@ -132,13 +118,7 @@ def unpack_single(self, context: _ContextLike) -> int:
data: list[int] = []
_, lb = self.bit_config(context)
shift = 0
- field: "Field" = context.get(CTX_FIELD)
- order: _EndianLike = (
- field.order
- if field
- else (self.__byteorder__ or O_DEFAULT_ENDIAN.value or LittleEndian)
- )
- is_little: bool = order.ch == LITTLE_ENDIAN_FMT
+ is_little = resolve_order(context, self).ch == LITTLE_ENDIAN_FMT
while True:
# Note tha unpack operation here to retrieve one byte only
diff --git a/src/caterpillar/model/__init__.py b/src/caterpillar/model/__init__.py
index e10431c7..9565e211 100644
--- a/src/caterpillar/model/__init__.py
+++ b/src/caterpillar/model/__init__.py
@@ -13,48 +13,41 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
from ._base import RemoveField, Sequence
-from ._struct import (
- Struct,
- struct,
- UnionHook,
- union,
- Invisible,
- StructDefMixin,
- struct_factory,
-)
from ._bitfield import (
+ DEFAULT_ALIGNMENT,
Bitfield,
- bitfield,
+ BitfieldDefMixin,
BitfieldEntry,
BitfieldGroup,
BitfieldValueFactory,
- issigned,
- getbits,
- NewGroup,
+ CharFactory,
EndGroup,
- SetAlignment,
EnumFactory,
- CharFactory,
- DEFAULT_ALIGNMENT,
+ NewGroup,
+ SetAlignment,
+ bitfield,
bitfield_factory,
- BitfieldDefMixin,
+ getbits,
+ issigned,
+)
+from ._struct import (
+ Invisible,
+ Struct,
+ StructDefMixin,
+ UnionHook,
+ struct,
+ struct_factory,
+ union,
)
from ._template import (
- istemplate,
- template,
+ TemplateFieldRef,
TemplateTypeVar,
derive,
field_of,
- TemplateFieldRef,
-)
-from .provider import (
- unpack,
- unpack_file,
- pack,
- pack_into,
- pack_file,
- sizeof,
+ istemplate,
+ template,
)
+from .provider import pack, pack_file, pack_into, sizeof, unpack, unpack_file
__all__ = [
"bitfield_factory",
diff --git a/src/caterpillar/model/_base.py b/src/caterpillar/model/_base.py
old mode 100755
new mode 100644
index 275addfe..985cb3f3
--- a/src/caterpillar/model/_base.py
+++ b/src/caterpillar/model/_base.py
@@ -13,52 +13,47 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
# pyright: reportPrivateUsage=false, reportAny=false, reportExplicitAny=false
+from caterpillar.byteorder import Inherit
import re
-
from collections.abc import Iterable
from typing import Annotated, Any, Generic, get_args, get_origin
-from typing_extensions import Self, override, TypeVar
+from typing_extensions import Self, TypeVar, override
+
+from caterpillar import registry
+from caterpillar._common import pack_seq, unpack_seq
+from caterpillar.abc import (
+ _ActionLike,
+ _ArchLike,
+ _ContextLambda,
+ _ContextLike,
+ _EndianLike,
+ _OptionLike,
+ _StreamType,
+ _StructLike,
+)
from caterpillar.context import (
CTX_FIELD,
- CTX_PATH,
CTX_OBJECT,
- CTX_STREAM,
+ CTX_PATH,
+ CTX_ROOT,
CTX_SEQ,
+ CTX_STREAM,
O_CONTEXT_FACTORY,
Context,
- CTX_ROOT,
+ CTX_PARENT,
+ CTX_ORDER,
)
from caterpillar.exception import StructException, ValidationError
+from caterpillar.fields import INVALID_DEFAULT, Const, Field, FieldMixin
from caterpillar.options import (
S_DISCARD_CONST,
S_DISCARD_UNNAMED,
- S_UNION,
S_REPLACE_TYPES,
+ S_UNION,
+ O_DEFAULT_STRUCT_ENDIAN,
)
-from caterpillar.fields import (
- Field,
- INVALID_DEFAULT,
- FieldMixin,
- Const,
-)
-from caterpillar._common import unpack_seq, pack_seq
-from caterpillar.shared import (
- ATTR_ACTION_PACK,
- ATTR_ACTION_UNPACK,
- Action,
-)
-from caterpillar import registry
-from caterpillar.abc import (
- _StructLike,
- _ContextLike,
- _OptionLike,
- _ContextLambda,
- _EndianLike,
- _ArchLike,
- _StreamType,
- _ActionLike,
-)
+from caterpillar.shared import ATTR_ACTION_PACK, ATTR_ACTION_UNPACK, Action
class _Member:
@@ -508,9 +503,44 @@ def __size__(self, context: _ContextLike) -> int:
context[CTX_PATH] = base_path
return max_size if self.is_union else total
+ def _resolve_order(self, context: _ContextLike) -> _EndianLike | None:
+ resolved = None
+ if self.order is not Inherit:
+ resolved = O_DEFAULT_STRUCT_ENDIAN.value
+ if resolved is None:
+ return None
+
+ # allow Inherit as well as BigEndian and LittleEndian default order
+ if resolved is Inherit:
+ # will be resolved in the next if-branch
+ resolved = None
+
+ if resolved is None:
+ parent = context.get(CTX_PARENT)
+ field = parent.get(CTX_FIELD) if parent is not None else None
+ resolved = None
+ if field is not None and field.has_order() and field.order is not Inherit:
+ # The field embedding this struct already carries a concrete,
+ # explicitly-resolved byte order
+ resolved = field.order
+ elif parent is not None:
+ # The enclosing struct is itself inheriting its order (chain of
+ # Inherit structs) - reuse whatever it already resolved for
+ # itself.
+ resolved = parent.get(CTX_ORDER)
+
+ if resolved is not None:
+ context[CTX_ORDER] = resolved
+ return resolved
+
def unpack_one(self, context: _ContextLike) -> _SeqOT:
# At first, we define the object context where the parsed values
# will be stored
+ if self.order is Inherit or (
+ self.order is None and O_DEFAULT_STRUCT_ENDIAN.value is not None
+ ):
+ _ = self._resolve_order(context)
+
factory = O_CONTEXT_FACTORY.value or Context
fields = self.fields
ctx_path = CTX_PATH
@@ -573,7 +603,9 @@ def __unpack__(self, context: _ContextLike) -> _SeqOT:
# See __pack__ for more information
field = context.get("_field")
if field and context[CTX_SEQ]:
- return unpack_seq(context, self.unpack_one) # pyright: ignore[reportReturnType]
+ return unpack_seq(
+ context, self.unpack_one
+ ) # pyright: ignore[reportReturnType]
return self.unpack_one(this_context)
def get_value(self, obj: _SeqIT, name: str, field: Field) -> Any | None:
@@ -587,6 +619,11 @@ def get_value(self, obj: _SeqIT, name: str, field: Field) -> Any | None:
return value
def pack_one(self, obj: _SeqIT, context: _ContextLike) -> None:
+ if self.order is Inherit or (
+ self.order is None and O_DEFAULT_STRUCT_ENDIAN.value is not None
+ ):
+ _ = self._resolve_order(context)
+
max_size = 0
union_field = None
fields = self.fields
diff --git a/src/caterpillar/model/_bitfield.py b/src/caterpillar/model/_bitfield.py
old mode 100755
new mode 100644
index c97a82b2..d44d66ed
--- a/src/caterpillar/model/_bitfield.py
+++ b/src/caterpillar/model/_bitfield.py
@@ -15,70 +15,59 @@
# pyright: reportPrivateUsage=false
import dataclasses
import enum
-
from collections.abc import Iterable
from typing import Any, Callable, Final, Generic, Literal
+
from typing_extensions import (
ClassVar,
Self,
+ TypeVar,
dataclass_transform,
overload,
override,
- TypeVar,
)
-from caterpillar.fields.common import Int
-from caterpillar.shared import (
- ATTR_ACTION_PACK,
- ATTR_ACTION_UNPACK,
- typeof,
- ATTR_BITS,
- ATTR_SIGNED,
+
+from caterpillar.abc import (
+ _ActionLike,
+ _ArchLike,
+ _ContextLike,
+ _EndianLike,
+ _OptionLike,
+ _StructLike,
)
-from caterpillar.byteorder import (
- LITTLE_ENDIAN_FMT,
- O_DEFAULT_ENDIAN,
- LittleEndian,
+from caterpillar.byteorder import LITTLE_ENDIAN_FMT, O_DEFAULT_ENDIAN, LittleEndian, Inherit
+from caterpillar.context import (
+ CTX_FIELD,
+ CTX_OBJECT,
+ CTX_PATH,
+ CTX_STREAM,
+ O_CONTEXT_FACTORY,
+ Context,
)
+from caterpillar.exception import StructException, ValidationError
+from caterpillar.fields import INVALID_DEFAULT, Field, Pass
+from caterpillar.fields.common import Int
from caterpillar.options import (
+ B_GROUP_END,
+ B_GROUP_KEEP,
B_GROUP_NEW,
+ B_NO_AUTO_BOOL,
+ B_OVERWRITE_ALIGNMENT,
GLOBAL_BITFIELD_FLAGS,
GLOBAL_STRUCT_OPTIONS,
GLOBAL_UNION_OPTIONS,
- B_OVERWRITE_ALIGNMENT,
- B_GROUP_END,
- B_GROUP_KEEP,
- B_NO_AUTO_BOOL,
Flag,
)
-from caterpillar.fields import (
- Field,
- Pass,
- INVALID_DEFAULT,
-)
-from caterpillar.exception import StructException, ValidationError
-from caterpillar.context import (
- CTX_FIELD,
- CTX_PATH,
- O_CONTEXT_FACTORY,
- CTX_OBJECT,
- CTX_STREAM,
- Context,
-)
-from caterpillar.abc import (
- _StructLike,
- _ActionLike,
- _ContextLike,
- _OptionLike,
- _ArchLike,
- _EndianLike,
-)
-from ._struct import (
- Struct,
- StructDefMixin,
- sizeof,
- Invisible,
+from caterpillar.shared import (
+ ATTR_ACTION_PACK,
+ ATTR_ACTION_UNPACK,
+ ATTR_BITS,
+ ATTR_SIGNED,
+ typeof,
)
+
from ._base import Sequence
+from ._struct import Invisible, Struct, StructDefMixin, sizeof
_AnnotationT = int | tuple[int, ...] | Any # pyright: ignore[reportExplicitAny]
_ModelT = TypeVar("_ModelT")
@@ -1025,6 +1014,11 @@ def __bits__(self) -> int:
@override
def unpack_one(self, context: _ContextLike) -> _VT:
+ # Resolves order=Inherit against the enclosing struct (no-op for
+ # bitfields using a fixed/unset byte order); see Sequence._resolve_order.
+ if self.order is Inherit:
+ _ = self._resolve_order(context)
+
init_data = (O_CONTEXT_FACTORY.value or Context)()
context[CTX_OBJECT] = (O_CONTEXT_FACTORY.value or Context)(_parent=context)
@@ -1083,6 +1077,11 @@ def unpack_one(self, context: _ContextLike) -> _VT:
@override
def pack_one(self, obj: _VT, context: _ContextLike) -> None:
+ # Resolves order=Inherit against the enclosing struct (no-op for
+ # bitfields using a fixed/unset byte order); see Sequence._resolve_order.
+ if self.order is Inherit:
+ _ = self._resolve_order(context)
+
base_path = context[CTX_PATH]
field: Field | None = context.get(CTX_FIELD)
members = self._members
diff --git a/src/caterpillar/model/_struct.py b/src/caterpillar/model/_struct.py
old mode 100755
new mode 100644
index 6ea758e0..6592f2f3
--- a/src/caterpillar/model/_struct.py
+++ b/src/caterpillar/model/_struct.py
@@ -13,47 +13,47 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
# pyright: reportAny=false, reportExplicitAny=false, reportPrivateUsage=false
-import inspect
import dataclasses as dc
-
-from io import BytesIO
+import inspect
from collections.abc import Collection, Iterable
-from typing import Any, Callable, Generic, Literal, ParamSpec, TypeVar
+from io import BytesIO
from types import TracebackType
+from typing import Any, Callable, Generic, Literal, ParamSpec, TypeVar
+
from typing_extensions import (
+ Buffer,
ClassVar,
Self,
dataclass_transform,
- override,
overload,
- Buffer,
+ override,
)
-from caterpillar.fields.conditional import apply_conditional_markers
-from caterpillar.shared import getstruct, hasstruct, ATTR_STRUCT, iscond
-from caterpillar.exception import InvalidValueError
-from caterpillar.options import (
- S_EVAL_ANNOTATIONS,
- S_UNION,
- S_ADD_BYTES,
- S_SLOTS,
- GLOBAL_STRUCT_OPTIONS,
- GLOBAL_UNION_OPTIONS,
-)
-from caterpillar.fields import Field, INVALID_DEFAULT
from caterpillar import registry
from caterpillar.abc import (
- _StreamType,
+ _ArchLike,
_ContextLike,
- _StructLike,
- _OptionLike,
_EndianLike,
- _ArchLike,
_LengthT,
+ _OptionLike,
+ _StreamType,
+ _StructLike, EndianLike,
)
-from .provider import unpack, pack, unpack_file, pack_into, sizeof
-from ._base import Sequence
+from caterpillar.exception import InvalidValueError
+from caterpillar.fields import INVALID_DEFAULT, Field
+from caterpillar.fields.conditional import apply_conditional_markers
+from caterpillar.options import (
+ GLOBAL_STRUCT_OPTIONS,
+ GLOBAL_UNION_OPTIONS,
+ S_ADD_BYTES,
+ S_EVAL_ANNOTATIONS,
+ S_SLOTS,
+ S_UNION,
+)
+from caterpillar.shared import ATTR_STRUCT, getstruct, hasstruct, iscond
+from ._base import Sequence
+from .provider import pack, pack_into, sizeof, unpack, unpack_file
_ModelT = TypeVar("_ModelT")
@@ -109,6 +109,11 @@ def __init__(
)
setattr(self.model, "__class_getitem__", _struct_getitem(self))
+ setattr(
+ self.model,
+ "__set_byteorder__",
+ classmethod(_struct_set_byteorder(self)),
+ )
if self.is_union:
# install a hook
self._union_hook: UnionHook[_ModelT] = (hook_cls or UnionHook)(self)
@@ -271,6 +276,17 @@ def class_getitem(*args: P.args, **kwargs: P.kwargs):
return class_getitem
+def _struct_set_byteorder(
+ model: Struct[_ModelT],
+) -> Callable[[type[_ModelT], _EndianLike], Field[_ModelT, _ModelT]]:
+ def set_byteorder(
+ cls: type[_ModelT], order: _EndianLike
+ ) -> Field[_ModelT, _ModelT]:
+ return Field(model, order=order)
+
+ return set_byteorder
+
+
# TODO: docs
def Invisible(*, init: bool = False, default: Any = None) -> Any:
"""Create a dataclass field that is hidden from the generated constructor.
@@ -333,8 +349,8 @@ class rather than requiring explicit imports of ``pack``/``unpack`` helpers.
"""A reference to the struct model of this class"""
def __class_getitem__(
- cls: type[_ModelT], dim: _LengthT
- ) -> Field[Collection[_ModelT], Collection[_ModelT]]:
+ cls: type[Self], dim: _LengthT
+ ) -> Field[Collection[Self], Collection[Self]]:
"""Enable ``cls[dim]`` syntax for defining repeated structure fields.
This method allows structure classes to be indexed using the ``[]``
@@ -344,19 +360,23 @@ def __class_getitem__(
:param dim: The length or dimension of the collection
:type dim: _LengthT
:return: A field descriptor representing a collection of the structure
- :rtype: Field[Collection[_ModelT], Collection[_ModelT]]
+ :rtype: Field[Collection[Self], Collection[Self]]
"""
return getstruct(cls)[dim]
+ @classmethod
+ def __set_byteorder__(cls, order: EndianLike) -> Field[Self, Self]:
+ return Field(cls, order=order)
+
@classmethod
def from_bytes(
- cls: type[_ModelT],
+ cls: type[Self],
data: Buffer | _StreamType,
*,
order: _EndianLike | None = None,
arch: _ArchLike | None = None,
**kwargs: Any,
- ) -> _ModelT:
+ ) -> Self:
"""Construct an instance from raw binary data or a stream.
This is a convenience wrapper around the underlying ``unpack``
@@ -370,19 +390,19 @@ def from_bytes(
:param arch: Architecture override for parsing, defaults to None
:type arch: _ArchLike | None, optional
:return: Parsed model instance
- :rtype: _ModelT
+ :rtype: Self
"""
return unpack(cls, data, order=order, arch=arch, **kwargs)
@classmethod
def from_file(
- cls: type[_ModelT],
+ cls: type[Self],
filename: str,
*,
order: _EndianLike | None = None,
arch: _ArchLike | None = None,
**kwargs: Any,
- ) -> _ModelT:
+ ) -> Self:
"""Construct an instance from a binary file on disk.
This is a convenience wrapper around ``unpack_file`` for reading and
@@ -395,7 +415,7 @@ def from_file(
:param arch: Architecture override for parsing, defaults to None
:type arch: _ArchLike | None, optional
:return: Parsed model instance
- :rtype: _ModelT
+ :rtype: Self
"""
return unpack_file(cls, filename, order=order, arch=arch, **kwargs)
diff --git a/src/caterpillar/model/_template.py b/src/caterpillar/model/_template.py
old mode 100755
new mode 100644
index 81959102..8dd3e32e
--- a/src/caterpillar/model/_template.py
+++ b/src/caterpillar/model/_template.py
@@ -14,38 +14,29 @@
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
-import sys
+import dataclasses
import inspect
+import sys
import types
-import dataclasses
-
from hashlib import md5
from types import ModuleType
-from typing import (
- Annotated,
- Any,
- Callable,
- Generic,
- TypeVar,
- get_args,
- get_origin,
-)
-from typing_extensions import overload, override, dataclass_transform
+from typing import Annotated, Any, Callable, Generic, TypeVar, get_args, get_origin
+
+from typing_extensions import dataclass_transform, overload, override
-from caterpillar.fields import Field, INVALID_DEFAULT
-from caterpillar.model import Invisible, Struct
-from caterpillar.options import S_UNION
-from caterpillar.shared import ATTR_TEMPLATE, hasstruct, typeof
from caterpillar.abc import (
- _LengthT,
- _StructLike,
+ _ArchLike,
_ContextLambda,
+ _EndianLike,
_GreedyType,
+ _LengthT,
+ _StructLike,
_SwitchLambda,
- _EndianLike,
- _ArchLike,
)
-
+from caterpillar.fields import INVALID_DEFAULT, Field
+from caterpillar.model import Invisible, Struct
+from caterpillar.options import S_UNION
+from caterpillar.shared import ATTR_TEMPLATE, hasstruct, typeof
_TYPEVAR_TYPE: type[TypeVar] = type(TypeVar("_CaterpillarTemplateTypeVar"))
diff --git a/src/caterpillar/model/provider.py b/src/caterpillar/model/provider.py
index ba72762c..a1dd6dba 100644
--- a/src/caterpillar/model/provider.py
+++ b/src/caterpillar/model/provider.py
@@ -13,37 +13,35 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
# pyright: reportAny=false, reportExplicitAny=false, reportPrivateUsage=false
-from tempfile import TemporaryFile
-from io import BytesIO, IOBase
from collections import OrderedDict
+from io import BytesIO, IOBase
from shutil import copyfileobj
+from tempfile import TemporaryFile
from typing import Any
-from typing_extensions import (
- overload,
- Buffer,
-)
+from typing_extensions import Buffer, overload
+
+from caterpillar.abc import (
+ _IT,
+ _OT,
+ ArchLike,
+ _ContainsStruct,
+ EndianLike,
+ _StreamType,
+ _SupportsPack,
+ _SupportsSize,
+ _SupportsUnpack,
+)
from caterpillar.byteorder import (
O_DEFAULT_ARCH,
O_DEFAULT_ENDIAN,
LittleEndian,
system_arch,
)
-from caterpillar.shared import ATTR_PACK, getstruct, hasstruct
-from caterpillar.context import O_CONTEXT_FACTORY, CTX_STREAM, Context
+from caterpillar.context import CTX_STREAM, O_CONTEXT_FACTORY, Context
from caterpillar.exception import DynamicSizeError
-from caterpillar.shared import MODE_PACK, MODE_UNPACK
-from caterpillar.abc import (
- _ContainsStruct,
- _OT,
- _IT,
- _SupportsPack,
- _StreamType,
- _SupportsUnpack,
- _SupportsSize,
- _EndianLike,
- _ArchLike,
-)
+from caterpillar.options import O_DEFAULT_STRUCT_ENDIAN
+from caterpillar.shared import ATTR_PACK, MODE_PACK, MODE_UNPACK, getstruct, hasstruct
@overload
@@ -54,9 +52,10 @@ def pack(
*,
use_tempfile: bool = False,
as_field: bool = False,
- order: _EndianLike | None = None,
- arch: _ArchLike | None = None,
+ order: EndianLike | None = None,
+ arch: ArchLike | None = None,
fill: int | bytes | str | None = None,
+ default_struct_order: EndianLike | None = None,
**kwargs: Any,
) -> bytes: ...
@overload
@@ -68,9 +67,10 @@ def pack(
*,
use_tempfile: bool = False,
as_field: bool = False,
- order: _EndianLike | None = None,
- arch: _ArchLike | None = None,
+ order: EndianLike | None = None,
+ arch: ArchLike | None = None,
fill: int | bytes | str | None = None,
+ default_struct_order: EndianLike | None = None,
**kwds: Any,
) -> bytes: ...
@overload
@@ -81,9 +81,10 @@ def pack(
*,
use_tempfile: bool = False,
as_field: bool = False,
- order: _EndianLike | None = None,
- arch: _ArchLike | None = None,
+ order: EndianLike | None = None,
+ arch: ArchLike | None = None,
fill: int | bytes | str | None = None,
+ default_struct_order: EndianLike | None = None,
**kwargs: Any,
) -> bytes: ...
@overload
@@ -95,9 +96,10 @@ def pack(
*,
use_tempfile: bool = False,
as_field: bool = False,
- order: _EndianLike | None = None,
- arch: _ArchLike | None = None,
+ order: EndianLike | None = None,
+ arch: ArchLike | None = None,
fill: int | bytes | str | None = None,
+ default_struct_order: EndianLike | None = None,
**kwargs: Any,
) -> bytes: ...
def pack(
@@ -107,9 +109,10 @@ def pack(
*,
use_tempfile: bool = False,
as_field: bool = False,
- order: _EndianLike | None = None,
- arch: _ArchLike | None = None,
+ order: EndianLike | None = None,
+ arch: ArchLike | None = None,
fill: int | bytes | str | None = None,
+ default_struct_order: EndianLike | None = None,
**kwargs: Any,
) -> bytes:
"""
@@ -139,6 +142,7 @@ def pack(
use_tempfile=use_tempfile,
as_field=as_field,
fill=fill,
+ default_struct_order=default_struct_order,
**kwargs,
)
return buffer.getvalue()
@@ -153,9 +157,10 @@ def pack_into(
*,
use_tempfile: bool = ...,
as_field: bool = ...,
- order: _EndianLike | None = ...,
- arch: _ArchLike | None = None,
+ order: EndianLike | None = ...,
+ arch: ArchLike | None = None,
fill: int | bytes | str | None = None,
+ default_struct_order: EndianLike | None = None,
**kwds: Any,
) -> None: ...
@overload
@@ -167,9 +172,10 @@ def pack_into(
*,
use_tempfile: bool = ...,
as_field: bool = ...,
- order: _EndianLike | None = ...,
- arch: _ArchLike | None = None,
+ order: EndianLike | None = ...,
+ arch: ArchLike | None = None,
fill: int | bytes | str | None = None,
+ default_struct_order: EndianLike | None = None,
**kwds: Any,
) -> None: ...
@overload
@@ -181,9 +187,10 @@ def pack_into(
*,
use_tempfile: bool = ...,
as_field: bool = ...,
- order: _EndianLike | None = ...,
- arch: _ArchLike | None = None,
+ order: EndianLike | None = ...,
+ arch: ArchLike | None = None,
fill: int | bytes | str | None = None,
+ default_struct_order: EndianLike | None = None,
**kwds: Any,
) -> None: ...
@overload
@@ -195,9 +202,10 @@ def pack_into(
*,
use_tempfile: bool = ...,
as_field: bool = ...,
- order: _EndianLike | None = ...,
- arch: _ArchLike | None = None,
+ order: EndianLike | None = ...,
+ arch: ArchLike | None = None,
fill: int | bytes | str | None = None,
+ default_struct_order: EndianLike | None = None,
**kwds: Any,
) -> None: ...
@overload
@@ -209,9 +217,10 @@ def pack_into(
*,
use_tempfile: bool = ...,
as_field: bool = ...,
- order: _EndianLike | None = ...,
- arch: _ArchLike | None = None,
+ order: EndianLike | None = ...,
+ arch: ArchLike | None = None,
fill: int | bytes | str | None = None,
+ default_struct_order: EndianLike | None = None,
**kwds: Any,
) -> None: ...
def pack_into(
@@ -222,9 +231,10 @@ def pack_into(
*,
use_tempfile: bool = False,
as_field: bool = False,
- order: _EndianLike | None = None,
- arch: _ArchLike | None = None,
+ order: EndianLike | None = None,
+ arch: ArchLike | None = None,
fill: int | bytes | str | None = None,
+ default_struct_order: EndianLike | None = None,
**kwds: Any,
) -> None:
"""
@@ -305,10 +315,14 @@ def pack_into(
prev_order = O_DEFAULT_ENDIAN.value
prev_arch = O_DEFAULT_ARCH.value
+ prev_sorder = O_DEFAULT_STRUCT_ENDIAN.value
if order:
O_DEFAULT_ENDIAN.value = order
if arch:
- O_DEFAULT_ARCH.value = prev_arch
+ O_DEFAULT_ARCH.value = arch
+ if default_struct_order:
+ O_DEFAULT_STRUCT_ENDIAN.value = default_struct_order
+
try:
start: int = 0
fill_pat: bytes = b"\x00"
@@ -363,6 +377,7 @@ def pack_into(
finally:
O_DEFAULT_ENDIAN.value = prev_order
O_DEFAULT_ARCH.value = prev_arch
+ O_DEFAULT_STRUCT_ENDIAN.value = prev_sorder
@overload
@@ -374,9 +389,10 @@ def pack_file(
*,
use_tempfile: bool = ...,
as_field: bool = ...,
- order: _EndianLike | None = ...,
- arch: _ArchLike | None = ...,
+ order: EndianLike | None = ...,
+ arch: ArchLike | None = ...,
fill: int | bytes | str | None = None,
+ default_struct_order: EndianLike | None = None,
**kwds: Any,
) -> None: ...
@overload
@@ -388,9 +404,10 @@ def pack_file(
*,
use_tempfile: bool = ...,
as_field: bool = ...,
- order: _EndianLike | None = ...,
- arch: _ArchLike | None = ...,
+ order: EndianLike | None = ...,
+ arch: ArchLike | None = ...,
fill: int | bytes | str | None = None,
+ default_struct_order: EndianLike | None = None,
**kwds: Any,
) -> None: ...
@overload
@@ -402,9 +419,10 @@ def pack_file(
*,
use_tempfile: bool = ...,
as_field: bool = ...,
- order: _EndianLike | None = ...,
- arch: _ArchLike | None = ...,
+ order: EndianLike | None = ...,
+ arch: ArchLike | None = ...,
fill: int | bytes | str | None = None,
+ default_struct_order: EndianLike | None = None,
**kwds: Any,
) -> None: ...
@overload
@@ -416,9 +434,10 @@ def pack_file(
*,
use_tempfile: bool = ...,
as_field: bool = ...,
- order: _EndianLike | None = ...,
- arch: _ArchLike | None = ...,
+ order: EndianLike | None = ...,
+ arch: ArchLike | None = ...,
fill: int | bytes | str | None = None,
+ default_struct_order: EndianLike | None = None,
**kwds: Any,
) -> None: ...
@overload
@@ -430,9 +449,10 @@ def pack_file(
*,
use_tempfile: bool = ...,
as_field: bool = ...,
- order: _EndianLike | None = ...,
- arch: _ArchLike | None = ...,
+ order: EndianLike | None = ...,
+ arch: ArchLike | None = ...,
fill: int | bytes | str | None = None,
+ default_struct_order: EndianLike | None = None,
**kwds: Any,
) -> None: ...
def pack_file(
@@ -443,9 +463,10 @@ def pack_file(
*,
use_tempfile: bool = False,
as_field: bool = False,
- order: _EndianLike | None = None,
- arch: _ArchLike | None = None,
+ order: EndianLike | None = None,
+ arch: ArchLike | None = None,
fill: int | bytes | str | None = None,
+ default_struct_order: EndianLike | None = None,
**kwds: Any,
) -> None:
"""
@@ -476,6 +497,7 @@ def pack_file(
order=order,
arch=arch,
fill=fill,
+ default_struct_order=default_struct_order,
**kwds,
)
@@ -487,8 +509,9 @@ def unpack(
/,
*,
as_field: bool = False,
- order: _EndianLike | None = None,
- arch: _ArchLike | None = None,
+ order: EndianLike | None = None,
+ arch: ArchLike | None = None,
+ default_struct_order: EndianLike | None = None,
**kwds: Any,
) -> _OT: ...
@overload
@@ -498,8 +521,9 @@ def unpack(
/,
*,
as_field: bool = False,
- order: _EndianLike | None = None,
- arch: _ArchLike | None = None,
+ order: EndianLike | None = None,
+ arch: ArchLike | None = None,
+ default_struct_order: EndianLike | None = None,
**kwds: Any,
) -> _OT: ...
@overload
@@ -509,8 +533,9 @@ def unpack(
/,
*,
as_field: bool = False,
- order: _EndianLike | None = None,
- arch: _ArchLike | None = None,
+ order: EndianLike | None = None,
+ arch: ArchLike | None = None,
+ default_struct_order: EndianLike | None = None,
**kwds: Any,
) -> _OT: ...
def unpack(
@@ -518,8 +543,9 @@ def unpack(
buffer: Buffer | _StreamType,
/,
as_field: bool = False,
- order: _EndianLike | None = None,
- arch: _ArchLike | None = None,
+ order: EndianLike | None = None,
+ arch: ArchLike | None = None,
+ default_struct_order: EndianLike | None = None,
**kwds: Any,
) -> _OT:
"""
@@ -572,16 +598,20 @@ def unpack(
prev_order = O_DEFAULT_ENDIAN.value
prev_arch = O_DEFAULT_ARCH.value
+ prev_sorder = O_DEFAULT_STRUCT_ENDIAN.value
if order:
O_DEFAULT_ENDIAN.value = order
if arch:
O_DEFAULT_ARCH.value = arch
+ if default_struct_order:
+ O_DEFAULT_STRUCT_ENDIAN.value = default_struct_order
try:
return struct.__unpack__(context)
finally:
O_DEFAULT_ARCH.value = prev_arch
O_DEFAULT_ENDIAN.value = prev_order
+ O_DEFAULT_STRUCT_ENDIAN.value = prev_sorder
@overload
@@ -591,8 +621,9 @@ def unpack_file(
/,
*,
as_field: bool = False,
- order: _EndianLike | None = None,
- arch: _ArchLike | None = None,
+ order: EndianLike | None = None,
+ arch: ArchLike | None = None,
+ default_struct_order: EndianLike | None = None,
**kwds: Any,
) -> _OT: ...
@overload
@@ -602,8 +633,9 @@ def unpack_file(
/,
*,
as_field: bool = False,
- order: _EndianLike | None = None,
- arch: _ArchLike | None = None,
+ order: EndianLike | None = None,
+ arch: ArchLike | None = None,
+ default_struct_order: EndianLike | None = None,
**kwds: Any,
) -> _OT: ...
@overload
@@ -613,8 +645,9 @@ def unpack_file(
/,
*,
as_field: bool = False,
- order: _EndianLike | None = None,
- arch: _ArchLike | None = None,
+ order: EndianLike | None = None,
+ arch: ArchLike | None = None,
+ default_struct_order: EndianLike | None = None,
**kwds: Any,
) -> _OT: ...
def unpack_file(
@@ -623,8 +656,9 @@ def unpack_file(
/,
*,
as_field: bool = False,
- order: _EndianLike | None = None,
- arch: _ArchLike | None = None,
+ order: EndianLike | None = None,
+ arch: ArchLike | None = None,
+ default_struct_order: EndianLike | None = None,
**kwds: Any,
):
"""
@@ -637,7 +671,15 @@ def unpack_file(
:return: The unpacked object.
"""
with open(filename, "rb") as fp:
- return unpack(struct, fp, as_field=as_field, arch=arch, order=order, **kwds)
+ return unpack(
+ struct,
+ fp,
+ as_field=as_field,
+ arch=arch,
+ order=order,
+ default_struct_order=default_struct_order,
+ **kwds,
+ )
@overload
diff --git a/src/caterpillar/options.py b/src/caterpillar/options.py
old mode 100755
new mode 100644
index 9df0bc30..84f373fc
--- a/src/caterpillar/options.py
+++ b/src/caterpillar/options.py
@@ -15,9 +15,10 @@
# pyright: reportPrivateUsage=false
from dataclasses import dataclass
from typing import Generic
+
from typing_extensions import Final, override
-from caterpillar.abc import _OptionLike, _VT, _ArrayFactoryLike
+from caterpillar.abc import _VT, _ArrayFactoryLike, _OptionLike, EndianLike
@dataclass(init=False, eq=False)
@@ -224,6 +225,7 @@ def get_flag(
S_UNION: Final[Flag] = Flag("struct.union")
S_ADD_BYTES: Final[Flag] = Flag("struct.bytes_method")
S_DISCARD_CONST: Final[Flag] = Flag("struct.discard_const")
+O_DEFAULT_STRUCT_ENDIAN: Final[Flag[EndianLike]] = Flag("struct.default_endian")
# for fields
F_KEEP_POSITION: Final[Flag] = Flag("field.keep_position")
diff --git a/src/caterpillar/py.py b/src/caterpillar/py.py
index fade08f6..a3905648 100644
--- a/src/caterpillar/py.py
+++ b/src/caterpillar/py.py
@@ -14,144 +14,135 @@
# along with this program. If not, see .
from typing import Annotated as f
-# use this module to import everything Python related
-from .registry import TypeConverter, annotation_registry, to_struct
+from ._common import WithoutContextVar, iseof, pack_seq, unpack_seq
from .byteorder import (
- ByteOrder,
- SysNative,
- Dynamic,
- DynByteOrder,
- Arch,
- system_arch,
- Native,
- BigEndian,
- LittleEndian,
- NetEndian,
AARCH64,
+ AMD,
+ AMD64,
ARM,
ARM64,
MIPS,
MIPS64,
- PowerPC,
- PowerPC64,
RISC_V,
RISC_V64,
SPARC,
SPARC64,
+ Arch,
+ BigEndian,
+ ByteOrder,
+ Dynamic,
+ DynByteOrder,
+ Inherit,
+ LittleEndian,
+ Native,
+ NetEndian,
+ PowerPC,
+ PowerPC64,
+ SysNative,
+ system_arch,
x86,
x86_64,
- AMD,
- AMD64,
)
from .context import (
- Context,
- ContextPath,
- ContextLength,
- ConditionContext,
- UnaryExpression,
- BinaryExpression,
- this,
- ctx,
- parent,
- CTX_PARENT,
- CTX_OBJECT,
- CTX_OFFSETS,
- CTX_STREAM,
+ CTX_ARCH,
CTX_FIELD,
- CTX_VALUE,
- CTX_POS,
CTX_INDEX,
+ CTX_OBJECT,
+ CTX_OFFSETS,
+ CTX_ORDER,
+ CTX_PARENT,
CTX_PATH,
- CTX_SEQ,
- CTX_ARCH,
+ CTX_POS,
CTX_ROOT,
- CTX_ORDER,
+ CTX_SEQ,
+ CTX_STREAM,
+ CTX_VALUE,
+ O_CONTEXT_FACTORY,
+ BinaryExpression,
+ ConditionContext,
+ Context,
+ ContextLength,
+ ContextPath,
ExprMixin,
- root,
SetContextVar,
- O_CONTEXT_FACTORY,
+ UnaryExpression,
+ ctx,
+ parent,
parentctx,
+ root,
+ this,
)
from .exception import (
- StructException,
- StreamError,
DelegationError,
+ DynamicSizeError,
InvalidValueError,
- Stop,
OptionError,
+ Stop,
+ StreamError,
+ StructException,
ValidationError,
- DynamicSizeError,
)
from .fields import * # noqa
from .model import * # noqa
from .options import (
- Flag,
- F_SEQUENTIAL,
+ B_GROUP_END,
+ B_GROUP_KEEP,
+ B_GROUP_NEW,
+ B_NO_AUTO_BOOL,
+ B_OVERWRITE_ALIGNMENT,
F_DYNAMIC,
F_KEEP_POSITION,
F_OFFSET_OVERRIDE,
- set_field_flags,
- set_struct_flags,
- set_union_flags,
+ F_SEQUENTIAL,
+ GLOBAL_BITFIELD_FLAGS,
+ GLOBAL_FIELD_FLAGS,
+ GLOBAL_STRUCT_OPTIONS,
+ GLOBAL_UNION_OPTIONS,
+ O_ARRAY_FACTORY,
S_ADD_BYTES,
S_DISCARD_CONST,
- S_SLOTS,
S_DISCARD_UNNAMED,
+ S_EVAL_ANNOTATIONS,
S_REPLACE_TYPES,
+ S_SLOTS,
S_UNION,
- S_EVAL_ANNOTATIONS,
- GLOBAL_BITFIELD_FLAGS,
- GLOBAL_STRUCT_OPTIONS,
- GLOBAL_UNION_OPTIONS,
- GLOBAL_FIELD_FLAGS,
+ Flag,
get_flag,
get_flags,
has_flag,
- O_ARRAY_FACTORY,
- B_GROUP_END,
- B_GROUP_KEEP,
- B_GROUP_NEW,
- B_NO_AUTO_BOOL,
- B_OVERWRITE_ALIGNMENT,
+ set_field_flags,
+ set_struct_flags,
+ set_union_flags,
)
-from ._common import WithoutContextVar, iseof, pack_seq, unpack_seq
+
+# use this module to import everything Python related
+from .registry import TypeConverter, annotation_registry, to_struct
from .shared import (
ATTR_ACTION_PACK,
- ATTR_STRUCT,
- Action,
ATTR_ACTION_UNPACK,
ATTR_BITS,
ATTR_BYTEORDER,
+ ATTR_PACK,
ATTR_SIGNED,
+ ATTR_STRUCT,
ATTR_TEMPLATE,
ATTR_TYPE,
- getstruct,
- hasstruct,
+ ATTR_UNPACK,
MODE_PACK,
MODE_UNPACK,
- typeof,
+ Action,
constval,
- ATTR_PACK,
- ATTR_UNPACK,
+ getstruct,
+ hasstruct,
+ typeof,
)
__all__ = [
"AARCH64",
- "Action",
- "Adler_Algo",
- "Adler_Field",
- "Adler",
- "Algorithm",
- "align",
- "Aligned",
"AMD",
"AMD64",
- "And",
- "annotation_registry",
- "Arch",
"ARM",
"ARM64",
- "AsLengthRef",
"ATTR_ACTION_PACK",
"ATTR_ACTION_UNPACK",
"ATTR_BITS",
@@ -167,260 +158,272 @@
"B_GROUP_NEW",
"B_NO_AUTO_BOOL",
"B_OVERWRITE_ALIGNMENT",
+ "CTX_ARCH",
+ "CTX_DIGEST",
+ "CTX_DIGEST_ALGO",
+ "CTX_DIGEST_HOOK",
+ "CTX_DIGEST_OBJ",
+ "CTX_FIELD",
+ "CTX_INDEX",
+ "CTX_OBJECT",
+ "CTX_OFFSETS",
+ "CTX_ORDER",
+ "CTX_PARENT",
+ "CTX_PATH",
+ "CTX_POS",
+ "CTX_ROOT",
+ "CTX_SEQ",
+ "CTX_STREAM",
+ "CTX_VALUE",
+ "DEFAULT_ALIGNMENT",
+ "DEFAULT_OPTION",
+ "ENUM_STRICT",
+ "F_DYNAMIC",
+ "F_KEEP_POSITION",
+ "F_OFFSET_OVERRIDE",
+ "F_SEQUENTIAL",
+ "GLOBAL_BITFIELD_FLAGS",
+ "GLOBAL_FIELD_FLAGS",
+ "GLOBAL_STRUCT_OPTIONS",
+ "GLOBAL_UNION_OPTIONS",
+ "HMAC",
+ "INVALID_DEFAULT",
+ "MAC",
+ "MIPS",
+ "MIPS64",
+ "MODE_PACK",
+ "MODE_UNPACK",
+ "O_ARRAY_FACTORY",
+ "O_CONTEXT_FACTORY",
+ "PTR_STRICT",
+ "RISC_V",
+ "RISC_V64",
+ "SPARC",
+ "SPARC64",
+ "S_ADD_BYTES",
+ "S_DISCARD_CONST",
+ "S_DISCARD_UNNAMED",
+ "S_EVAL_ANNOTATIONS",
+ "S_REPLACE_TYPES",
+ "S_SLOTS",
+ "S_UNION",
+ "VARINT_LSB",
+ "Action",
+ "Adler",
+ "Adler_Algo",
+ "Adler_Field",
+ "Algorithm",
+ "Aligned",
+ "And",
+ "Arch",
+ "AsLengthRef",
"BigEndian",
"BinaryExpression",
- "bitfield_factory",
- "bitfield",
"Bitfield",
"BitfieldEntry",
"BitfieldGroup",
"BitfieldValueFactory",
- "boolean",
"Branch",
"ByteOrder",
"Bytes",
"Bz2Compressed",
+ "CString",
"Chain",
- "char",
"CharFactory",
"Compressed",
"Computed",
- "ConditionalChain",
"ConditionContext",
+ "ConditionalChain",
"Const",
"ConstBytes",
"ConstString",
- "constval",
"Context",
"ContextLength",
"ContextPath",
+ "Crc32",
"Crc32_Algo",
"Crc32_Field",
- "Crc32",
- "CString",
- "CTX_ARCH",
- "CTX_DIGEST_ALGO",
- "CTX_DIGEST_HOOK",
- "CTX_DIGEST_OBJ",
- "CTX_DIGEST",
- "CTX_FIELD",
- "CTX_INDEX",
- "CTX_OBJECT",
- "CTX_OFFSETS",
- "CTX_ORDER",
- "CTX_PARENT",
- "CTX_PATH",
- "CTX_POS",
- "CTX_ROOT",
- "CTX_SEQ",
- "CTX_STREAM",
- "CTX_VALUE",
- "ctx",
- "DEFAULT_ALIGNMENT",
- "DEFAULT_OPTION",
"DelegationError",
- "derive",
"Digest",
"DigestField",
"DigestFieldAction",
- "double",
+ "DynByteOrder",
"Dynamic",
"DynamicSizeError",
- "DynByteOrder",
"Else",
"ElseIf",
"Encrypted",
"End",
"EndGroup",
- "ENUM_STRICT",
"Enum",
"EnumFactory",
"ExprMixin",
- "F_DYNAMIC",
- "F_KEEP_POSITION",
- "F_OFFSET_OVERRIDE",
- "F_SEQUENTIAL",
- "f",
- "field_of",
"Field",
"FieldMixin",
"FieldStruct",
"Flag",
- "float16",
- "float32",
- "float64",
- "get_args",
- "get_flag",
- "get_flags",
- "get_kwargs",
- "getbits",
- "getstruct",
- "GLOBAL_BITFIELD_FLAGS",
- "GLOBAL_FIELD_FLAGS",
- "GLOBAL_STRUCT_OPTIONS",
- "GLOBAL_UNION_OPTIONS",
- "has_flag",
- "hasstruct",
- "HMAC",
"HMACAlgorithm",
+ "IOHook",
+ "IPv4Address",
+ "IPv6Address",
"If",
+ "Inherit",
"Int",
- "int16",
- "int24",
- "int32",
- "int64",
- "int8",
- "intptr_fn",
- "intptr",
- "INVALID_DEFAULT",
"InvalidValueError",
"Invisible",
- "IOHook",
- "IPv4Address",
- "IPv6Address",
- "iseof",
- "issigned",
- "istemplate",
"KeyCipher",
- "Lazy",
- "LittleEndian",
"LZMACompressed",
"LZOCompressed",
- "MAC",
+ "Lazy",
+ "LittleEndian",
"MACAddress",
+ "Md5",
"Md5_Algo",
"Md5_Field",
- "Md5",
"Memory",
- "MIPS",
- "MIPS64",
- "MODE_PACK",
- "MODE_UNPACK",
"Native",
"NetEndian",
"NewGroup",
- "O_ARRAY_FACTORY",
- "O_CONTEXT_FACTORY",
- "offintptr",
- "offuintptr",
"Operator",
"OptionError",
"Or",
"Otherwise",
- "pack_file",
- "pack_into",
- "pack_seq",
- "pack",
"Padded",
- "padding",
"Padding",
- "parent",
- "parentctx",
"Pass",
- "pointer",
"Pointer",
"PostPad",
"PowerPC",
"PowerPC64",
- "Prefixed",
"PrePad",
- "psize",
- "pssize",
- "PTR_STRICT",
+ "Prefixed",
"PyStructFormattedField",
- "relative_pointer",
"RelativePointer",
"RemoveField",
- "RISC_V",
- "RISC_V64",
- "root",
- "S_ADD_BYTES",
- "S_DISCARD_CONST",
- "S_DISCARD_UNNAMED",
- "S_EVAL_ANNOTATIONS",
- "S_REPLACE_TYPES",
- "S_SLOTS",
- "S_UNION",
"Sequence",
- "set_field_flags",
- "set_struct_flags",
- "set_union_flags",
"SetAlignment",
"SetContextVar",
+ "Sha1",
"Sha1_Algo",
"Sha1_Field",
- "Sha1",
+ "Sha2_224",
"Sha2_224_Algo",
"Sha2_224_Field",
- "Sha2_224",
+ "Sha2_256",
"Sha2_256_Algo",
"Sha2_256_Field",
- "Sha2_256",
+ "Sha2_384",
"Sha2_384_Algo",
"Sha2_384_Field",
- "Sha2_384",
+ "Sha2_512",
"Sha2_512_Algo",
"Sha2_512_Field",
- "Sha2_512",
+ "Sha3_224",
"Sha3_224_Algo",
"Sha3_224_Field",
- "Sha3_224",
+ "Sha3_256",
"Sha3_256_Algo",
"Sha3_256_Field",
- "Sha3_256",
+ "Sha3_384",
"Sha3_384_Algo",
"Sha3_384_Field",
- "Sha3_384",
+ "Sha3_512",
"Sha3_512_Algo",
"Sha3_512_Field",
- "Sha3_512",
- "singleton",
- "sizeof",
- "SPARC",
- "SPARC64",
"Start",
"Stop",
"StreamError",
"String",
- "struct_factory",
- "struct",
"Struct",
"StructDefMixin",
"StructException",
"SysNative",
- "system_arch",
- "template",
"TemplateFieldRef",
"TemplateTypeVar",
- "this",
- "to_struct",
"Transformer",
"TypeConverter",
- "typeof",
"UInt",
+ "UnaryExpression",
+ "UnionHook",
+ "Uuid",
+ "ValidationError",
+ "VarInt",
+ "When",
+ "WithoutContextVar",
+ "Xor",
+ "ZLibCompressed",
+ "align",
+ "annotation_registry",
+ "bitfield",
+ "bitfield_factory",
+ "boolean",
+ "char",
+ "constval",
+ "ctx",
+ "derive",
+ "double",
+ "f",
+ "field_of",
+ "float16",
+ "float32",
+ "float64",
+ "get_args",
+ "get_flag",
+ "get_flags",
+ "get_kwargs",
+ "getbits",
+ "getstruct",
+ "has_flag",
+ "hasstruct",
+ "int8",
+ "int16",
+ "int24",
+ "int32",
+ "int64",
+ "intptr",
+ "intptr_fn",
+ "iseof",
+ "issigned",
+ "istemplate",
+ "offintptr",
+ "offuintptr",
+ "pack",
+ "pack_file",
+ "pack_into",
+ "pack_seq",
+ "padding",
+ "parent",
+ "parentctx",
+ "pointer",
+ "psize",
+ "pssize",
+ "relative_pointer",
+ "root",
+ "set_field_flags",
+ "set_struct_flags",
+ "set_union_flags",
+ "singleton",
+ "sizeof",
+ "struct",
+ "struct_factory",
+ "system_arch",
+ "template",
+ "this",
+ "to_struct",
+ "typeof",
+ "uint8",
"uint16",
"uint24",
"uint32",
"uint64",
- "uint8",
- "uintptr_fn",
"uintptr",
- "UnaryExpression",
+ "uintptr_fn",
"union",
- "UnionHook",
+ "unpack",
"unpack_file",
"unpack_seq",
- "unpack",
- "Uuid",
- "ValidationError",
- "VARINT_LSB",
- "VarInt",
"vint",
"void_ptr",
- "When",
- "WithoutContextVar",
- "x86_64",
"x86",
- "Xor",
- "ZLibCompressed",
+ "x86_64",
]
diff --git a/src/caterpillar/registry.py b/src/caterpillar/registry.py
index 340c7d6c..5f4e4dfb 100644
--- a/src/caterpillar/registry.py
+++ b/src/caterpillar/registry.py
@@ -43,6 +43,7 @@
"""
from typing import Any, Callable
+
from typing_extensions import override
from caterpillar.abc import _StructLike
diff --git a/src/caterpillar/shared.py b/src/caterpillar/shared.py
index 986a8b60..de55cda9 100644
--- a/src/caterpillar/shared.py
+++ b/src/caterpillar/shared.py
@@ -42,20 +42,21 @@
and won't be stored as part of the struct model.
"""
-from typing import TYPE_CHECKING, overload, Generic, Any
-from typing_extensions import Final, Literal, override, TypeIs, Buffer
+from typing import TYPE_CHECKING, Any, Generic, overload
+
+from typing_extensions import Buffer, Final, Literal, TypeIs, override
from caterpillar.abc import (
- _ContextLambda,
- _StructLike,
_IT,
_OT,
- _SupportsType,
- _ContainsStruct,
_ActionLike,
_ArchLike,
+ _ContainsStruct,
+ _ContextLambda,
_EndianLike,
_StreamType,
+ _StructLike,
+ _SupportsType,
)
if TYPE_CHECKING:
@@ -416,7 +417,7 @@ def to_bytes(
:rtype: bytes | None
"""
# fmt: off
- from caterpillar.model import pack_into, pack
+ from caterpillar.model import pack, pack_into
if fp is not None:
pack_into( # pyright: ignore[reportCallIssue]
obj,
@@ -522,12 +523,10 @@ def from_file(
"""
from caterpillar.model import unpack_file
- return (
- unpack_file( # pyright: ignore[reportCallIssue, reportUnknownVariableType]
- self, # pyright: ignore[reportArgumentType]
- filename,
- order=order,
- arch=arch,
- **kwargs,
- )
+ return unpack_file( # pyright: ignore[reportCallIssue, reportUnknownVariableType]
+ self, # pyright: ignore[reportArgumentType]
+ filename,
+ order=order,
+ arch=arch,
+ **kwargs,
)
diff --git a/src/caterpillar/shortcuts.py b/src/caterpillar/shortcuts.py
index d91c80da..3fe3974f 100644
--- a/src/caterpillar/shortcuts.py
+++ b/src/caterpillar/shortcuts.py
@@ -13,40 +13,45 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
from typing import Annotated as f
+
+from . import options as opt
from .byteorder import (
AARCH64,
AMD,
AMD64,
ARM,
ARM64,
+ RISC_V,
+ RISC_V64,
BigEndian,
- LittleEndian,
Dynamic,
+ Inherit,
+ LittleEndian,
PowerPC,
PowerPC64,
- RISC_V,
- RISC_V64,
x86,
x86_64,
)
-from .context import ContextPath, ctx, parent, this, ContextLength as lenof
-from .context import ctx as C, parent as P, root as G
+from .context import ContextLength as lenof
+from .context import ContextPath, ctx, parent, this
+from .context import ctx as C
+from .context import parent as P
+from .context import root as G
+from .fields import Field as F
+from .model import Sequence as Seq
from .model import (
bitfield,
pack,
pack_file,
pack_into,
+ sizeof,
struct,
union,
unpack,
unpack_file,
- sizeof,
- Sequence as Seq,
)
-from .shared import typeof, getstruct, hasstruct
from .registry import to_struct
-from . import options as opt
-from .fields import Field as F
+from .shared import getstruct, hasstruct, typeof
__all__ = [
"AARCH64",
@@ -54,38 +59,39 @@
"AMD64",
"ARM",
"ARM64",
+ "RISC_V",
+ "RISC_V64",
"BigEndian",
+ "C",
+ "ContextPath",
+ "Dynamic",
+ "F",
+ "G",
+ "Inherit",
"LittleEndian",
+ "P",
"PowerPC",
"PowerPC64",
- "RISC_V",
- "RISC_V64",
- "x86",
- "x86_64",
+ "Seq",
"bitfield",
+ "ctx",
+ "f",
+ "getstruct",
+ "hasstruct",
+ "lenof",
+ "opt",
"pack",
"pack_file",
"pack_into",
+ "parent",
+ "sizeof",
"struct",
+ "this",
+ "to_struct",
+ "typeof",
"union",
"unpack",
"unpack_file",
- "sizeof",
- "Seq",
- "typeof",
- "getstruct",
- "hasstruct",
- "ContextPath",
- "ctx",
- "parent",
- "this",
- "lenof",
- "to_struct",
- "F",
- "opt",
- "C",
- "P",
- "G",
- "Dynamic",
- "f",
+ "x86",
+ "x86_64",
]
diff --git a/src/caterpillar/types.py b/src/caterpillar/types.py
index 4a20b0f5..3bb24172 100644
--- a/src/caterpillar/types.py
+++ b/src/caterpillar/types.py
@@ -12,37 +12,37 @@
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
-from datetime import datetime
import ipaddress
import uuid
+from datetime import datetime
from .fields.common import (
+ CString,
+ Pass,
+ Timestamp,
+ Uuid,
boolean,
char,
- uint8,
- uint16,
- uint24,
- uint32,
- uint64,
+ double,
+ float16,
+ float32,
+ float64,
int8,
int16,
int24,
int32,
int64,
- float16,
- float32,
- float64,
- double,
psize,
pssize,
- Uuid,
- Pass,
- CString,
- Timestamp,
+ uint8,
+ uint16,
+ uint24,
+ uint32,
+ uint64,
void_ptr,
)
-from .fields.varint import vint
from .fields.net import MAC, IPv4Address, IPv6Address
+from .fields.varint import vint
from .shortcuts import f
bool1_t = f[bool, 1]
diff --git a/src/code_gen/genapi.py b/src/code_gen/genapi.py
index eca41938..d6ed6170 100644
--- a/src/code_gen/genapi.py
+++ b/src/code_gen/genapi.py
@@ -3,18 +3,12 @@
=====================================================
"""
-import pathlib
import argparse
import dataclasses
-from typing_extensions import override
+import pathlib
-from caterpillar_api import (
- CP_FUNC_API,
- CP_TYPE_API,
- cp_api_functions,
- CP_SRC,
- CP_TYPES,
-)
+from caterpillar_api import CP_FUNC_API, CP_SRC, CP_TYPE_API, CP_TYPES, cp_api_functions
+from typing_extensions import override
# Base class for all API objects (functions or types)
@@ -50,7 +44,7 @@ def cp_internal_def(self) -> str:
if self.type.startswith("Py"):
return f"extern {self.type} {self.name};"
- extra_def = f"#define {self.name.replace('_Type', '')}_NAME \"{self.type}\""
+ extra_def = f'#define {self.name.replace("_Type", "")}_NAME "{self.type}"'
return f"extern PyTypeObject {self.name};\n{extra_def}"
@override
diff --git a/test/_C/test_atom.py b/test/_C/test_atom.py
index 75ea104c..2482fc80 100644
--- a/test/_C/test_atom.py
+++ b/test/_C/test_atom.py
@@ -1,5 +1,6 @@
# pylint: disable=unused-import, no-name-in-module
import pytest
+
import caterpillar
if caterpillar.native_support():
diff --git a/test/_C/test_conditional.py b/test/_C/test_conditional.py
index ad2ea2e6..e38a987b 100644
--- a/test/_C/test_conditional.py
+++ b/test/_C/test_conditional.py
@@ -1,9 +1,10 @@
import pytest
+
import caterpillar
if caterpillar.native_support():
from caterpillar._C import Conditional
- from caterpillar.py import ctx, pack, unpack, Bytes, sizeof
+ from caterpillar.py import Bytes, ctx, pack, sizeof, unpack
def testc_conditional_init():
# simple declarative switch parsing
diff --git a/test/_C/test_context.py b/test/_C/test_context.py
index c18329c1..7d5ed4b0 100644
--- a/test/_C/test_context.py
+++ b/test/_C/test_context.py
@@ -1,5 +1,6 @@
# pylint: disable=unused-import, no-name-in-module
import pytest
+
import caterpillar
if caterpillar.native_support():
diff --git a/test/_C/test_endian.py b/test/_C/test_endian.py
index e36777f7..d542a5a6 100644
--- a/test/_C/test_endian.py
+++ b/test/_C/test_endian.py
@@ -1,4 +1,5 @@
import pytest
+
import caterpillar
if caterpillar.native_support():
diff --git a/test/_C/test_offset.py b/test/_C/test_offset.py
index b412964c..15eba8f3 100644
--- a/test/_C/test_offset.py
+++ b/test/_C/test_offset.py
@@ -1,10 +1,12 @@
+import io
+
import pytest
+
import caterpillar
-import io
if caterpillar.native_support():
from caterpillar._C import AtOffset
- from caterpillar.py import Bytes, pack, unpack, sizeof, ctx
+ from caterpillar.py import Bytes, ctx, pack, sizeof, unpack
# Offset initialization can be either done manually or by
# using the '@' (matmul) operator on subclasses of BuiltinAtom.
diff --git a/test/_C/test_option.py b/test/_C/test_option.py
index d3e435dc..e375872d 100644
--- a/test/_C/test_option.py
+++ b/test/_C/test_option.py
@@ -1,9 +1,10 @@
import pytest
+
import caterpillar
if caterpillar.native_support():
from caterpillar.c import c_Option
- from caterpillar.py import Flag, Field
+ from caterpillar.py import Field, Flag
def testc_option_init():
option = c_Option("name")
diff --git a/test/_C/test_protocols.py b/test/_C/test_protocols.py
index 07efbb42..302baf60 100644
--- a/test/_C/test_protocols.py
+++ b/test/_C/test_protocols.py
@@ -1,29 +1,29 @@
# pyright: reportPrivateUsage=false
import pytest
+
import caterpillar
if caterpillar.native_support():
+ from caterpillar.abc import (
+ _ArchLike,
+ _ContextLike,
+ _EndianLike,
+ _OptionLike,
+ _StructLike,
+ )
from caterpillar.c import (
- c_Arch,
- c_Context,
- c_Endian,
- c_Option,
AtOffset,
Atom,
BuiltinAtom,
Conditional,
Repeated,
- Switch
- )
- from caterpillar.abc import (
- _StructLike,
- _OptionLike,
- _EndianLike,
- _ArchLike,
- _ContextLike,
+ Switch,
+ c_Arch,
+ c_Context,
+ c_Endian,
+ c_Option,
)
-
# === PROTOCOL COMPLIANCE ===
# All tests below should verify the protocol compliance of all classes
# defined in this library. New ones should be added here too.
@@ -48,6 +48,6 @@
],
)
def test_py_protocol(obj: type, proto_ty: type) -> None:
- assert isinstance(
- obj, proto_ty
- ), f"Class {obj.__name__} does not conform to the {proto_ty.__name__} protocol"
+ assert isinstance(obj, proto_ty), (
+ f"Class {obj.__name__} does not conform to the {proto_ty.__name__} protocol"
+ )
diff --git a/test/_C/test_repeated.py b/test/_C/test_repeated.py
index 9b978395..651c7ba4 100644
--- a/test/_C/test_repeated.py
+++ b/test/_C/test_repeated.py
@@ -1,10 +1,10 @@
import pytest
-import caterpillar
+import caterpillar
if caterpillar.native_support():
from caterpillar._C import Repeated
- from caterpillar.py import uint8, pack, unpack, Bytes, sizeof, typeof
+ from caterpillar.py import Bytes, pack, sizeof, typeof, uint8, unpack
def testc_repeated_init():
atom = Repeated(uint8, 3)
diff --git a/test/_C/test_switch.py b/test/_C/test_switch.py
index a36c331a..4e8146dd 100644
--- a/test/_C/test_switch.py
+++ b/test/_C/test_switch.py
@@ -1,14 +1,14 @@
-import pytest
-import caterpillar
import enum
-from caterpillar.shared import typeof
+import pytest
+import caterpillar
+from caterpillar.shared import typeof
if caterpillar.native_support():
from caterpillar._C import Switch
- from caterpillar.py import Bytes, String, ctx, pack, unpack
from caterpillar.exception import ValidationError
+ from caterpillar.py import Bytes, String, ctx, pack, unpack
def testc_switch_init():
# simple declarative switch parsing
@@ -61,6 +61,6 @@ def testc_switch_type():
{
b"a": String(1),
b"b": String(2),
- }
+ },
)
- assert typeof(atom) == object | str
\ No newline at end of file
+ assert typeof(atom) == object | str
diff --git a/test/_Py/fields/test_py_aligned.py b/test/_Py/fields/test_py_aligned.py
index 0e634cb1..6afe6639 100644
--- a/test/_Py/fields/test_py_aligned.py
+++ b/test/_Py/fields/test_py_aligned.py
@@ -2,7 +2,18 @@
import pytest
-from caterpillar.py import Aligned, Bytes, f, pack, struct, this, uint8, uint16, uint32, unpack
+from caterpillar.py import (
+ Aligned,
+ Bytes,
+ f,
+ pack,
+ struct,
+ this,
+ uint8,
+ uint16,
+ uint32,
+ unpack,
+)
from caterpillar.types import uint8_t
diff --git a/test/_Py/fields/test_py_chain.py b/test/_Py/fields/test_py_chain.py
index 1af253d9..bf69f250 100644
--- a/test/_Py/fields/test_py_chain.py
+++ b/test/_Py/fields/test_py_chain.py
@@ -1,4 +1,4 @@
-from caterpillar.py import Bytes, CTX_STREAM, Chain, Context, FieldStruct, pack, unpack
+from caterpillar.py import CTX_STREAM, Bytes, Chain, Context, FieldStruct, pack, unpack
class SuffixTransform(FieldStruct[bytes, bytes]):
diff --git a/test/_Py/fields/test_py_compressed.py b/test/_Py/fields/test_py_compressed.py
index a98f5fdb..e5f4f9a1 100644
--- a/test/_Py/fields/test_py_compressed.py
+++ b/test/_Py/fields/test_py_compressed.py
@@ -1,17 +1,16 @@
-from io import BytesIO
-
import lzma
import zlib
+from io import BytesIO
import pytest
-from caterpillar.py import Bytes, pack, root, unpack
from caterpillar.fields.compression import (
Bz2Compressed,
Compressed,
LZMACompressed,
ZLibCompressed,
)
+from caterpillar.py import Bytes, pack, root, unpack
@pytest.mark.parametrize("payload", [b"", b"hello hello hello", bytes(range(256))])
diff --git a/test/_Py/fields/test_py_const.py b/test/_Py/fields/test_py_const.py
index 6dddc18e..58c7819a 100644
--- a/test/_Py/fields/test_py_const.py
+++ b/test/_Py/fields/test_py_const.py
@@ -1,7 +1,7 @@
import pytest
-from caterpillar.py import ConstBytes, Invisible, f, struct, unpack, pack, Const, uint8
from caterpillar.exception import ValidationError
+from caterpillar.py import Const, ConstBytes, Invisible, f, pack, struct, uint8, unpack
def test_const_pack():
diff --git a/test/_Py/fields/test_py_cstring.py b/test/_Py/fields/test_py_cstring.py
index 2b66e248..2e77c2fc 100644
--- a/test/_Py/fields/test_py_cstring.py
+++ b/test/_Py/fields/test_py_cstring.py
@@ -2,7 +2,7 @@
import pytest
-from caterpillar.py import CString, pack, unpack, ValidationError
+from caterpillar.py import CString, ValidationError, pack, unpack
def test_fixed_cstring():
diff --git a/test/_Py/fields/test_py_digest.py b/test/_Py/fields/test_py_digest.py
index d9727147..b4397bab 100644
--- a/test/_Py/fields/test_py_digest.py
+++ b/test/_Py/fields/test_py_digest.py
@@ -1,18 +1,19 @@
+import sys
import typing
+
import pytest
-import sys
from caterpillar.exception import ValidationError
-from caterpillar.py import Bytes, Struct, unpack, pack, struct
from caterpillar.fields.digest import (
DigestField,
Md5,
+ Md5_Algo,
Md5_Field,
Sha2_256,
- Md5_Algo,
Sha2_256_Algo,
Sha2_256_Field,
)
+from caterpillar.py import Bytes, Struct, pack, struct, unpack
from caterpillar.shared import ATTR_ACTION_PACK, getstruct
from caterpillar.shortcuts import f
diff --git a/test/_Py/fields/test_py_encrypted.py b/test/_Py/fields/test_py_encrypted.py
index 07df98c6..1cabc12b 100644
--- a/test/_Py/fields/test_py_encrypted.py
+++ b/test/_Py/fields/test_py_encrypted.py
@@ -7,9 +7,8 @@
from cryptography.hazmat.primitives import padding
from cryptography.hazmat.primitives.ciphers import algorithms, modes
-from caterpillar.py import ValidationError, pack, root, unpack
from caterpillar.fields.crypto import Encrypted
-
+from caterpillar.py import ValidationError, pack, root, unpack
AES_KEY = b"\x00" * 16
AES_WRONG_KEY = b"\xff" * 16
diff --git a/test/_Py/fields/test_py_if.py b/test/_Py/fields/test_py_if.py
index db618b4d..27fac3c6 100644
--- a/test/_Py/fields/test_py_if.py
+++ b/test/_Py/fields/test_py_if.py
@@ -12,10 +12,10 @@
If,
Invisible,
LittleEndian,
+ Otherwise,
Start,
StructException,
When,
- Otherwise,
f,
pack,
struct,
diff --git a/test/_Py/fields/test_py_keycipher.py b/test/_Py/fields/test_py_keycipher.py
index 009cf1d3..172b85c7 100644
--- a/test/_Py/fields/test_py_keycipher.py
+++ b/test/_Py/fields/test_py_keycipher.py
@@ -1,7 +1,7 @@
import pytest
-from caterpillar.py import InvalidValueError, pack, root, unpack
from caterpillar.fields.crypto import KeyCipher
+from caterpillar.py import InvalidValueError, pack, root, unpack
class ToyXor(KeyCipher):
diff --git a/test/_Py/fields/test_py_macaddress.py b/test/_Py/fields/test_py_macaddress.py
index fc17285b..90757675 100644
--- a/test/_Py/fields/test_py_macaddress.py
+++ b/test/_Py/fields/test_py_macaddress.py
@@ -4,7 +4,6 @@
from caterpillar.py import MACAddress, ValidationError, pack, unpack
-
RAW = b"\xaa\xbb\xcc\xdd\xee\xff"
diff --git a/test/_Py/fields/test_py_memory.py b/test/_Py/fields/test_py_memory.py
index c738e321..8dbcb841 100644
--- a/test/_Py/fields/test_py_memory.py
+++ b/test/_Py/fields/test_py_memory.py
@@ -3,11 +3,11 @@
from caterpillar.py import (
DynamicSizeError,
Memory,
+ ValidationError,
pack,
root,
sizeof,
unpack,
- ValidationError,
)
diff --git a/test/_Py/fields/test_py_offset.py b/test/_Py/fields/test_py_offset.py
index 8c62d199..5acad9e4 100644
--- a/test/_Py/fields/test_py_offset.py
+++ b/test/_Py/fields/test_py_offset.py
@@ -1,13 +1,6 @@
import pytest
-from caterpillar.py import (
- f,
- struct,
- pack,
- unpack,
- uint8,
- this,
-)
+from caterpillar.py import f, pack, struct, this, uint8, unpack
def test_fixed_offset_unpack():
diff --git a/test/_Py/fields/test_py_padding.py b/test/_Py/fields/test_py_padding.py
index 87dd61d8..e8b42af4 100644
--- a/test/_Py/fields/test_py_padding.py
+++ b/test/_Py/fields/test_py_padding.py
@@ -5,20 +5,20 @@
from caterpillar.py import (
DynamicSizeError,
Padded,
+ Padding,
PostPad,
PrePad,
ValidationError,
constval,
f,
- padding,
pack,
+ padding,
sizeof,
struct,
this,
uint8,
uint16,
unpack,
- Padding,
)
from caterpillar.types import uint8_t
diff --git a/test/_Py/fields/test_py_pass.py b/test/_Py/fields/test_py_pass.py
index 1ad971a5..c296f7d3 100644
--- a/test/_Py/fields/test_py_pass.py
+++ b/test/_Py/fields/test_py_pass.py
@@ -1,9 +1,4 @@
-from caterpillar.py import (
- struct,
- unpack,
- pack,
- Pass,
-)
+from caterpillar.py import Pass, pack, struct, unpack
from caterpillar.types import pass_t
diff --git a/test/_Py/fields/test_py_pointer.py b/test/_Py/fields/test_py_pointer.py
index 19350e0c..5eee4385 100644
--- a/test/_Py/fields/test_py_pointer.py
+++ b/test/_Py/fields/test_py_pointer.py
@@ -2,7 +2,15 @@
import pytest
-from caterpillar.py import Pointer, StructException, pack, pointer, struct, uint8, unpack
+from caterpillar.py import (
+ Pointer,
+ StructException,
+ pack,
+ pointer,
+ struct,
+ uint8,
+ unpack,
+)
@struct
diff --git a/test/_Py/fields/test_py_pystruct.py b/test/_Py/fields/test_py_pystruct.py
index c50db97f..0db40af8 100644
--- a/test/_Py/fields/test_py_pystruct.py
+++ b/test/_Py/fields/test_py_pystruct.py
@@ -75,22 +75,32 @@ def test_fixed_array_uses_requested_endianness():
def test_float_formats():
assert pack(1.5, float16, order=LittleEndian) == pystruct.pack("e", 1.5)
- assert unpack(float16, pystruct.pack("f", 3.25)
- assert unpack(float32, pystruct.pack("d", 3.25)
- assert unpack(float64, pystruct.pack(">d", 3.25), order=BigEndian) == pytest.approx(3.25)
+ assert unpack(float64, pystruct.pack(">d", 3.25), order=BigEndian) == pytest.approx(
+ 3.25
+ )
# --------------------------------------------------------------------------- #
# validation and native-only formats
# --------------------------------------------------------------------------- #
def test_packing_wrong_types_raises():
- for field, value in ((uint16, "not an int"), (float32, "not a float"), (char, b"too long")):
+ for field, value in (
+ (uint16, "not an int"),
+ (float32, "not a float"),
+ (char, b"too long"),
+ ):
with pytest.raises(pystruct.error):
pack(value, field)
@@ -108,5 +118,3 @@ def test_native_only_formats_force_native_endian():
assert pack(42, pssize) == pystruct.pack("@n", 42)
assert pack(42, psize) == pystruct.pack("@N", 42)
assert pack(0x1000, void_ptr) == pystruct.pack("@P", 0x1000)
-
-
diff --git a/test/_Py/model/test_bitfield.py b/test/_Py/model/test_bitfield.py
index ab490628..75a0811e 100644
--- a/test/_Py/model/test_bitfield.py
+++ b/test/_Py/model/test_bitfield.py
@@ -2,21 +2,19 @@
import pytest
+from caterpillar.fields import Bytes, uint8, uint16
from caterpillar.model import (
Bitfield,
- bitfield,
+ CharFactory,
EndGroup,
+ Invisible,
SetAlignment,
+ bitfield,
+ pack,
sizeof,
unpack,
- CharFactory,
- pack,
- Invisible
-)
-from caterpillar.options import (
- S_REPLACE_TYPES,
)
-from caterpillar.fields import uint16, Bytes, uint8
+from caterpillar.options import S_REPLACE_TYPES
from caterpillar.py import BigEndian, LittleEndian, ValidationError, int8
from caterpillar.shared import getstruct
from caterpillar.shortcuts import f
diff --git a/test/_Py/model/test_template.py b/test/_Py/model/test_template.py
index 1d1398f0..4fe87528 100644
--- a/test/_Py/model/test_template.py
+++ b/test/_Py/model/test_template.py
@@ -1,15 +1,15 @@
-from typing_extensions import TypeVar, Generic
+from typing_extensions import Generic, TypeVar
from caterpillar.py import (
BigEndian,
derive,
f,
field_of,
+ pack,
struct,
template,
- uint16,
uint8,
- pack,
+ uint16,
unpack,
)
from caterpillar.types import uint8_t
diff --git a/test/_Py/test_arch.py b/test/_Py/test_arch.py
index f845b569..57e04d25 100644
--- a/test/_Py/test_arch.py
+++ b/test/_Py/test_arch.py
@@ -10,8 +10,8 @@
struct,
system_arch,
uint8,
- unpack,
uintptr,
+ unpack,
x86,
x86_64,
)
diff --git a/test/_Py/test_byteorder.py b/test/_Py/test_byteorder.py
index 7274e5b9..17829cbf 100644
--- a/test/_Py/test_byteorder.py
+++ b/test/_Py/test_byteorder.py
@@ -1,23 +1,31 @@
+from caterpillar.abc import ContextLike
+from caterpillar.byteorder import Inherit
+from caterpillar.context import SetContextVar
+from caterpillar.fields import Field
from caterpillar.py import (
- Invisible,
- struct,
- pack,
- uint64,
- unpack,
- uint16,
- uint32,
+ CTX_ORDER,
+ BigEndian,
Dynamic,
DynByteOrder,
- BigEndian,
+ Invisible,
LittleEndian,
- CTX_ORDER,
+ Pointer,
+ Prefixed,
+ bitfield,
ctx,
- this,
f,
+ pack,
+ parent,
+ struct,
+ this,
+ uint8,
+ uint16,
+ uint32,
+ uint64,
+ unpack,
+ StructDefMixin,
)
-from caterpillar.types import uint16_t, uint32_t, uint64_t, uint8_t
-from caterpillar.context import SetContextVar
-from caterpillar.abc import _ContextLike # pyright: ignore[reportPrivateUsage]
+from caterpillar.types import uint8_t, uint16_t, uint32_t, uint64_t
def test_byteorder_pack_explicit():
@@ -186,7 +194,7 @@ class Format:
def test_dyn_byteorder_action():
# byteorder based on a custom pre-computed context value
- def byteorder_func(context: _ContextLike): # pyright: ignore[reportUnusedParameter]
+ def byteorder_func(context: ContextLike): # pyright: ignore[reportUnusedParameter]
# custom implementation...
return LittleEndian
@@ -203,3 +211,303 @@ class Format:
assert data.hex() == "1234bc9a7856"
+def test_dyn_byteorder_grandparent_context():
+ @struct(order=Dynamic(parent.parent.value))
+ class Inner:
+ value: uint32_t
+
+ @struct
+ class Middle:
+ inner: Inner
+
+ @struct
+ class Outer:
+ value: uint8_t
+ middle: Middle
+
+ big_endian = Outer(0, Middle(Inner(0x11223344)))
+ little_endian = Outer(1, Middle(Inner(0x11223344)))
+
+ big_data = bytes.fromhex("0011223344")
+ little_data = bytes.fromhex("0144332211")
+ assert pack(big_endian) == big_data
+ assert unpack(Outer, big_data) == big_endian
+ assert pack(little_endian) == little_data
+ assert unpack(Outer, little_data) == little_endian
+
+
+def test_inherit_byteorder_struct():
+ # order=Inherit makes a struct resolve its own byte order from
+ # whichever struct embeds it, instead of a fixed order.
+ @struct(order=Inherit)
+ class Inner:
+ value: uint32_t
+
+ @struct(order=BigEndian)
+ class Outer:
+ inner: Inner
+
+ @struct(order=LittleEndian)
+ class Format:
+ inner: Inner
+
+ data = bytes.fromhex("11223344")
+ assert unpack(Outer, data).inner.value == 0x11223344
+ assert pack(Outer(Inner(0x11223344))) == data
+
+ assert unpack(Format, data).inner.value == 0x44332211
+ assert pack(Format(Inner(0x44332211))) == data
+
+
+def test_inherit_byteorder_standalone_fallback():
+ # A struct declared with order=Inherit but used without an enclosing
+ # struct (e.g. passed directly to pack/unpack) falls back to the
+ # regular default byte order, exactly like order=None.
+ @struct(order=Inherit)
+ class Inner:
+ value: uint32_t
+
+ data = bytes.fromhex("11223344")
+ assert unpack(Inner, data).value == 0x44332211 # little-endian default
+ assert pack(Inner(0x44332211)) == data
+
+
+def test_inherit_byteorder_sibling():
+ @struct(order=Inherit)
+ class Inner:
+ value: uint32_t
+
+ @struct
+ class Format:
+ little: LittleEndian + Inner
+ big: BigEndian + Inner
+
+ data = bytes.fromhex("11223344") * 2
+ obj = unpack(Format, data)
+ assert obj.little.value == 0x44332211
+ assert obj.big.value == 0x11223344
+ assert pack(obj) == data
+
+
+def test_inherit_byteorder_mix():
+ @struct(order=Inherit)
+ class Inner(StructDefMixin):
+ value: uint32_t
+
+ @struct(order=BigEndian)
+ class Outer:
+ little: LittleEndian + Inner
+ big: BigEndian + Inner
+ inherited: Inner # follows Outer's own order (BigEndian)
+
+ data = bytes.fromhex("11223344") * 3
+ obj = unpack(Outer, data)
+ assert obj.little.value == 0x44332211
+ assert obj.big.value == 0x11223344
+ assert obj.inherited.value == 0x11223344
+ assert pack(obj) == data
+
+ # Flipping the parent's own order only moves the plain reference;
+ # the two explicit overrides stay pinned to their declared order.
+ @struct(order=LittleEndian)
+ class Format:
+ little: LittleEndian + Inner
+ big: BigEndian + Inner
+ inherited: Inner # now follows Format's LittleEndian
+
+ obj = unpack(Format, data)
+ assert obj.little.value == 0x44332211
+ assert obj.big.value == 0x11223344
+ assert obj.inherited.value == 0x44332211
+ assert pack(obj) == data
+
+
+def test_inherit_byteorder_array():
+ # order=Inherit resolves the same way for array members.
+ @struct(order=Inherit)
+ class Inner(StructDefMixin):
+ value: uint32_t
+
+ @struct(order=BigEndian)
+ class Outer:
+ items: Inner[2]
+
+ data = bytes.fromhex("11223344") * 2
+ obj = unpack(Outer, data)
+ assert [item.value for item in obj.items] == [0x11223344, 0x11223344]
+ assert pack(obj) == data
+
+
+def test_inherit_byteorder_chain():
+ # Multi-level Inherit chains resolve:
+ # Outer(BigEndian) -> Middle(Inherit) -> Inner(Inherit)
+ @struct(order=Inherit)
+ class Inner:
+ value: uint32_t
+
+ @struct(order=Inherit)
+ class Middle:
+ inner: Inner
+
+ @struct(order=BigEndian)
+ class Outer:
+ middle: Middle
+
+ data = bytes.fromhex("11223344")
+ obj = unpack(Outer, data)
+ assert obj.middle.inner.value == 0x11223344
+ assert pack(obj) == data
+
+
+def test_inherit_byteorder_mixed_chain():
+ # A chain that mixes explicit orders and Inherit at different depths must
+ # resolve each Inherit level against the *nearest* concrete ancestor,
+ # not the outermost one: Outer(BE) -> Middle(LE, explicit) -> Inner(Inherit)
+ # Inner must follow Middle's LE, not Outer's BE.
+ @struct(order=Inherit)
+ class Inner:
+ v: uint32_t
+
+ @struct(order=LittleEndian)
+ class Middle:
+ v: Inner
+
+ @struct(order=BigEndian)
+ class Outer:
+ v: Middle
+
+ data = bytes.fromhex("11223344")
+ obj = unpack(Outer, data)
+ assert obj.v.v.v == 0x44332211
+ assert pack(obj) == data
+
+
+def test_inherit_byteorder_prefixed():
+ @struct(order=Inherit)
+ class Inner:
+ value: uint32_t
+
+ @struct(order=BigEndian)
+ class Outer:
+ inner: Prefixed(uint8, Inner)
+
+ obj = Outer(Inner(0x11223344))
+ packed = pack(obj)
+ assert packed == bytes.fromhex("04") + bytes.fromhex("11223344")
+ assert unpack(Outer, packed).inner.value == 0x11223344
+
+
+def test_inherit_byteorder_explicit_inner_order():
+ @struct(order=LittleEndian)
+ class Inner:
+ value_le: uint64_t
+
+ @struct(order=BigEndian)
+ class Format:
+ inner: Inner
+
+ value = 0x123456789ABCDEF
+ raw_le = bytes.fromhex("efcdab8967452301")
+ obj = Format(Inner(value))
+ assert pack(obj) == raw_le
+ assert unpack(Format, raw_le) == obj
+
+
+def test_inherit_byteorder_annotated_override():
+ # f[...] annotations can override the order for a single field, just
+ # like they already do for plain atoms (see test_byteorder_struct_explicit).
+ @struct(order=Inherit)
+ class Inner:
+ value: uint32_t
+
+ @struct(order=BigEndian)
+ class Outer:
+ a: f[Inner, Inner, LittleEndian]
+
+ data = bytes.fromhex("11223344")
+ assert unpack(Outer, data).a.value == 0x44332211
+
+
+def test_inherit_byteorder_bitfield():
+ # Bitfield supports order=Inherit exactly like a regular struct.
+ @bitfield(order=Inherit)
+ class Inner:
+ a: f[int, 5]
+ b: f[int, 7]
+
+ @struct(order=BigEndian)
+ class Outer:
+ bits: Inner
+
+ @struct(order=LittleEndian)
+ class Format:
+ bits: Inner
+
+ obj = Inner(a=0b10101, b=0b1100110)
+ data_be = bytes.fromhex("ae60")
+ data_le = bytes.fromhex("60ae")
+
+ assert pack(Outer(obj)) == data_be
+ assert unpack(Outer, data_be).bits == obj
+
+ assert pack(Format(obj)) == data_le
+ assert unpack(Format, data_le).bits == obj
+
+
+def test_inherit_byteorder_prefixed_nested_bitfield():
+ # An explicitly ordered length field must not become the embedding order
+ # for a deeply nested Inherit payload.
+ @bitfield(order=Inherit)
+ class InnerBits:
+ value: f[int, 16]
+
+ @struct(order=Inherit)
+ class Inner:
+ bits: InnerBits
+
+ @struct(order=BigEndian)
+ class Outer:
+ inner: Prefixed(LittleEndian + uint16, Inner)
+
+ data = bytes.fromhex("02001122")
+ obj = unpack(Outer, data)
+ assert obj.inner.bits.value == 0x1122
+ assert pack(Outer(Inner(InnerBits(0x1122)))) == data
+
+
+def test_inherit_byteorder_pointer_target_after_ordered_pointer():
+ # The pointer representation can have its own order without changing the
+ # order inherited by the object it points to.
+ @struct(order=Inherit)
+ class Inner:
+ value: uint32_t
+
+ @struct(order=BigEndian)
+ class Outer:
+ inner: Pointer(LittleEndian + uint16, Inner)
+
+ data = bytes.fromhex("020011223344")
+ obj = unpack(Outer, data)
+ assert obj.inner.obj.value == 0x11223344
+
+ obj.inner.obj = Inner(0x11223344)
+ assert pack(obj) == data
+
+
+def test_inherit_byteorder_switch_restores_embedding_field():
+ @struct(order=Inherit)
+ class Inner:
+ value: uint32_t
+
+ discriminator = Field(LittleEndian + uint16, options={1: Inner})
+
+ @struct(order=BigEndian)
+ class Format:
+ inner: discriminator
+
+ @struct(order=BigEndian)
+ class Outer:
+ inner: Field(lambda _context: 1, options={1: Inner})
+
+ assert unpack(Format, bytes.fromhex("010011223344")).inner.value == 0x11223344
+ assert unpack(Outer, bytes.fromhex("11223344")).inner.value == 0x11223344
diff --git a/test/_Py/test_contextpath.py b/test/_Py/test_contextpath.py
index 9fef658d..29379c03 100644
--- a/test/_Py/test_contextpath.py
+++ b/test/_Py/test_contextpath.py
@@ -2,7 +2,9 @@
import pytest
-from caterpillar.py import Context, ctx as context_path, f, parent, pack, root, struct, this, uint8, unpack
+from caterpillar.py import Context
+from caterpillar.py import ctx as context_path
+from caterpillar.py import f, pack, parent, root, struct, this, uint8, unpack
def sample_context():
diff --git a/test/_Py/test_flag.py b/test/_Py/test_flag.py
index a991cb5b..26900795 100644
--- a/test/_Py/test_flag.py
+++ b/test/_Py/test_flag.py
@@ -1,7 +1,7 @@
from caterpillar.options import (
+ GLOBAL_STRUCT_OPTIONS,
Flag,
configure,
- GLOBAL_STRUCT_OPTIONS,
get_flag,
get_flags,
)
diff --git a/test/_Py/test_protocols.py b/test/_Py/test_protocols.py
index 2fb250c0..49973c80 100644
--- a/test/_Py/test_protocols.py
+++ b/test/_Py/test_protocols.py
@@ -1,60 +1,60 @@
# pyright: reportPrivateUsage=false
import pytest
+from caterpillar.abc import (
+ _ActionLike,
+ _ArchLike,
+ _ContextLambda,
+ _ContextLike,
+ _EndianLike,
+ _OptionLike,
+ _StructLike,
+)
from caterpillar.py import (
+ Aligned,
BinaryExpression,
Bitfield,
+ Bytes,
Chain,
Compressed,
+ Computed,
ConditionalChain,
+ Const,
ConstBytes,
ConstString,
+ Context,
ContextLength,
ContextPath,
+ CString,
DigestField,
DigestFieldAction,
+ Dynamic,
Encrypted,
Enum,
Field,
+ Flag,
+ Int,
IPv4Address,
IPv6Address,
- Int,
KeyCipher,
Lazy,
MACAddress,
+ Memory,
+ Pass,
Pointer,
+ Prefixed,
PyStructFormattedField,
RelativePointer,
Sequence,
+ String,
Struct,
SysNative,
- Const,
- Dynamic,
Transformer,
UInt,
- Flag,
UnaryExpression,
+ Uuid,
VarInt,
system_arch,
- Context,
- String,
- Bytes,
- Memory,
- Computed,
- Pass,
- CString,
- Prefixed,
- Uuid,
- Aligned,
-)
-from caterpillar.abc import (
- _StructLike,
- _OptionLike,
- _EndianLike,
- _ArchLike,
- _ContextLike,
- _ContextLambda,
- _ActionLike,
)
@@ -119,6 +119,6 @@
],
)
def test_py_protocol(obj: type, proto_ty: type) -> None:
- assert isinstance(
- obj, proto_ty
- ), f"Class {obj.__name__} does not conform to the {proto_ty.__name__} protocol"
+ assert isinstance(obj, proto_ty), (
+ f"Class {obj.__name__} does not conform to the {proto_ty.__name__} protocol"
+ )
diff --git a/test/_Py/test_registry.py b/test/_Py/test_registry.py
index 8e703f8e..0542a7f5 100644
--- a/test/_Py/test_registry.py
+++ b/test/_Py/test_registry.py
@@ -1,10 +1,10 @@
import pytest
from caterpillar import registry
+from caterpillar.byteorder import BigEndian
from caterpillar.exception import ValidationError
-from caterpillar.fields import uint16, uint32, Bytes, Const, Field
+from caterpillar.fields import Bytes, Const, Field, uint16, uint32
from caterpillar.model import getbits, struct
-from caterpillar.byteorder import BigEndian
def test_registry_invalid_add():
diff --git a/test/__init__.py b/test/__init__.py
index b1de4272..e6aa0104 100644
--- a/test/__init__.py
+++ b/test/__init__.py
@@ -1 +1 @@
-from . import _Py
\ No newline at end of file
+from . import _Py
diff --git a/test/requirements.txt b/test/requirements.txt
index 245e665f..7c75d536 100644
--- a/test/requirements.txt
+++ b/test/requirements.txt
@@ -1,3 +1,4 @@
pytest
black
-tox
\ No newline at end of file
+tox
+pytest-benchmark
\ No newline at end of file