From ff505e02bdcfb004ff06713840e46eda474d55ed Mon Sep 17 00:00:00 2001 From: MatrixEditor <58256046+MatrixEditor@users.noreply.github.com> Date: Sat, 1 Aug 2026 09:16:19 +0200 Subject: [PATCH 1/8] feat(abc): expose public protocol aliases - fix benchmark example --- benchmark/test_comparison_example.py | 9 ++- src/caterpillar/abc.py | 87 ++++++++++++++++++++++------ test/requirements.txt | 3 +- 3 files changed, 77 insertions(+), 22 deletions(-) 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/src/caterpillar/abc.py b/src/caterpillar/abc.py index 632c6ee2..dd048dda 100755 --- 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, + runtime_checkable, + TypeAlias, +) +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/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 From 7eace53d140b45f00b97dbae7a7d54fb6ad6f7fc Mon Sep 17 00:00:00 2001 From: MatrixEditor <58256046+MatrixEditor@users.noreply.github.com> Date: Sat, 1 Aug 2026 09:19:12 +0200 Subject: [PATCH 2/8] refactor: reformat files and imports --- .../snippets/comparison_1_caterpillar.py | 4 +- examples/bitfield_example.py | 11 +- examples/c_extension/main.py | 2 +- examples/formats/caf.py | 4 +- examples/formats/nibarchive.py | 8 +- examples/greedy_example.py | 1 + examples/inline_syntax.py | 2 +- examples/mixin_example.py | 2 +- examples/operator_example.py | 7 +- examples/pointer_example.py | 3 +- src/caterpillar/_C.pyi | 19 +- src/caterpillar/__init__.py | 3 +- src/caterpillar/__main__.py | 2 +- src/caterpillar/_common.py | 35 +--- src/caterpillar/abc.py | 11 +- src/caterpillar/byteorder.py | 102 ++++++++--- src/caterpillar/context.py | 34 ++-- src/caterpillar/fields/__init__.py | 164 ++++-------------- src/caterpillar/fields/_base.py | 60 ++----- src/caterpillar/fields/_mixin.py | 34 ++-- src/caterpillar/fields/common.py | 68 +++----- src/caterpillar/fields/compression.py | 13 +- src/caterpillar/fields/conditional.py | 21 +-- src/caterpillar/fields/crypto.py | 25 +-- src/caterpillar/fields/digest.py | 14 +- src/caterpillar/fields/hook.py | 5 +- src/caterpillar/fields/net.py | 17 +- src/caterpillar/fields/pointer.py | 19 +- src/caterpillar/fields/varint.py | 18 +- src/caterpillar/model/__init__.py | 52 ++---- src/caterpillar/model/_base.py | 57 ++---- src/caterpillar/model/_bitfield.py | 80 +++------ src/caterpillar/model/_struct.py | 52 ++---- src/caterpillar/model/_template.py | 35 ++-- src/caterpillar/model/provider.py | 40 ++--- src/caterpillar/options.py | 3 +- src/caterpillar/py.py | 149 ++++------------ src/caterpillar/registry.py | 1 + src/caterpillar/shared.py | 38 ++-- src/caterpillar/shortcuts.py | 49 ++---- src/caterpillar/types.py | 33 +--- src/code_gen/genapi.py | 15 +- test/_C/test_atom.py | 1 + test/_C/test_conditional.py | 3 +- test/_C/test_context.py | 1 + test/_C/test_endian.py | 1 + test/_C/test_offset.py | 6 +- test/_C/test_option.py | 3 +- test/_C/test_protocols.py | 32 +--- test/_C/test_repeated.py | 4 +- test/_C/test_switch.py | 12 +- test/_Py/fields/test_py_aligned.py | 3 +- test/_Py/fields/test_py_bytes.py | 3 +- test/_Py/fields/test_py_chain.py | 3 +- test/_Py/fields/test_py_compressed.py | 11 +- test/_Py/fields/test_py_computed.py | 3 +- test/_Py/fields/test_py_const.py | 3 +- test/_Py/fields/test_py_cstring.py | 2 +- test/_Py/fields/test_py_digest.py | 16 +- test/_Py/fields/test_py_encrypted.py | 3 +- test/_Py/fields/test_py_enum.py | 3 +- test/_Py/fields/test_py_if.py | 25 +-- test/_Py/fields/test_py_int.py | 3 +- test/_Py/fields/test_py_keycipher.py | 2 +- test/_Py/fields/test_py_lengthfor.py | 15 +- test/_Py/fields/test_py_macaddress.py | 1 - test/_Py/fields/test_py_memory.py | 11 +- test/_Py/fields/test_py_offset.py | 9 +- test/_Py/fields/test_py_padding.py | 21 +-- test/_Py/fields/test_py_pass.py | 7 +- test/_Py/fields/test_py_pointer.py | 3 +- test/_Py/fields/test_py_prefixed.py | 12 +- test/_Py/fields/test_py_pystruct.py | 48 ++--- test/_Py/model/test_bitfield.py | 30 +--- test/_Py/model/test_template.py | 18 +- test/_Py/test_arch.py | 17 +- test/_Py/test_byteorder.py | 26 +-- test/_Py/test_contextpath.py | 4 +- test/_Py/test_flag.py | 9 +- test/_Py/test_protocols.py | 74 ++------ test/_Py/test_registry.py | 4 +- test/__init__.py | 2 +- 82 files changed, 563 insertions(+), 1203 deletions(-) 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/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/src/caterpillar/_C.pyi b/src/caterpillar/_C.pyi index bcdd8668..3af33b49 100644 --- a/src/caterpillar/_C.pyi +++ b/src/caterpillar/_C.pyi @@ -15,22 +15,15 @@ # pyright: reportExplicitAny=false, reportAny=false, reportPrivateUsage=false from collections.abc import Collection from typing import Any, Generic -from typing_extensions import override, Self -from caterpillar.abc import ( - _OT, - _IT, - _EndianLike, - _SupportsSetEndian, - _ContextLike, - _LengthT, - _StructLike, - _ContextLambda, - _SwitchOptionsT, -) + +from typing_extensions import Self, override + from caterpillar import native_support +from caterpillar.abc import (_IT, _OT, _ContextLambda, _ContextLike, + _EndianLike, _LengthT, _StructLike, + _SupportsSetEndian, _SwitchOptionsT) 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 index 9b5c4fad..de9b8bd0 100755 --- a/src/caterpillar/_common.py +++ b/src/caterpillar/_common.py @@ -13,37 +13,18 @@ # 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, - _LengthT, - _SupportsPack, -) -from caterpillar.context import ( - CTX_PATH, - CTX_FIELD, - CTX_INDEX, - CTX_OBJECT, - CTX_STREAM, - CTX_SEQ, - O_CONTEXT_FACTORY, - Context, -) -from caterpillar.exception import ( - Stop, - StructException, - InvalidValueError, - ValidationError, -) +from caterpillar.abc import (_IT, _OT, _ContextLike, _LengthT, _PrefixedType, + _StreamType, _SupportsPack) +from caterpillar.context import (CTX_FIELD, CTX_INDEX, CTX_OBJECT, CTX_PATH, + CTX_SEQ, CTX_STREAM, O_CONTEXT_FACTORY, + Context) +from caterpillar.exception import (InvalidValueError, Stop, StructException, + ValidationError) from caterpillar.options import O_ARRAY_FACTORY if TYPE_CHECKING: diff --git a/src/caterpillar/abc.py b/src/caterpillar/abc.py index dd048dda..6249bf5c 100755 --- a/src/caterpillar/abc.py +++ b/src/caterpillar/abc.py @@ -16,15 +16,8 @@ from collections.abc import Callable, Collection, Iterable from io import IOBase from types import EllipsisType, NoneType -from typing import ( - TYPE_CHECKING, - Any, - Final, - Literal, - Protocol, - runtime_checkable, - TypeAlias, -) +from typing import (TYPE_CHECKING, Any, Final, Literal, Protocol, TypeAlias, + runtime_checkable) from typing_extensions import Buffer, TypeVar, overload, override diff --git a/src/caterpillar/byteorder.py b/src/caterpillar/byteorder.py index 88f3c8cc..f28e392b 100755 --- a/src/caterpillar/byteorder.py +++ b/src/caterpillar/byteorder.py @@ -14,25 +14,20 @@ # 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 caterpillar.abc import ( - _EndianLike, - _SupportsSetEndian, - _OT, - _ContextLambda, - _ContextLike, - _ArchLike, -) -from caterpillar.shared import ATTR_BYTEORDER +from platform import machine +from sys import maxsize +from typing import Final + +from typing_extensions import override + +from caterpillar.abc import (_OT, ArchLike, ContextLambda, ContextLike, + EndianLike, SupportsSetEndian) from caterpillar.context import CTX_ORDER from caterpillar.options import Flag +from caterpillar.shared import ATTR_BYTEORDER @dataclass(frozen=True) @@ -72,7 +67,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 +147,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 +174,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 +203,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 +227,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 +243,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 +260,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 +335,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 +394,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 index 2fc186b5..551ab559 100755 --- a/src/caterpillar/context.py +++ b/src/caterpillar/context.py @@ -19,34 +19,20 @@ import sys import typing import warnings - -from typing import Annotated, Callable, Any, Generic, Protocol, get_args, get_origin -from typing_extensions import ( - Buffer, - Final, - Literal, - Self, - Sized, - overload, - override, - TypeVar, -) -from types import FrameType, TracebackType from dataclasses import dataclass +from types import FrameType, TracebackType +from typing import (Annotated, Any, Callable, Generic, Protocol, get_args, + get_origin) +from typing_extensions import (Buffer, Final, Literal, Self, Sized, TypeVar, + overload, override) + +from caterpillar.abc import (_IT, _OT, _ArchLike, _ContextFactoryLike, + _ContextLambda, _ContextLike, _EndianLike, + _StreamType) from caterpillar.exception import StructException -from caterpillar.registry import to_struct from caterpillar.options import Flag -from caterpillar.abc import ( - _ContextLike, - _ContextLambda, - _IT, - _ContextFactoryLike, - _OT, - _StreamType, - _EndianLike, - _ArchLike, -) +from caterpillar.registry import to_struct from caterpillar.shared import iscond if typing.TYPE_CHECKING: diff --git a/src/caterpillar/fields/__init__.py b/src/caterpillar/fields/__init__.py index e4728042..9db8f7c7 100644 --- a/src/caterpillar/fields/__init__.py +++ b/src/caterpillar/fields/__init__.py @@ -12,139 +12,39 @@ # # 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 .common import ( - PyStructFormattedField, - Transformer, - Const, - ConstBytes, - ConstString, - Enum, - String, - Bytes, - Memory, - Computed, - Pass, - CString, - Prefixed, - Int, - UInt, - Padding, - AsLengthRef, - Timestamp, - padding, - char, - boolean, - int8, - uint8, - int16, - uint16, - int24, - uint24, - int32, - uint32, - int64, - uint64, - pssize, - psize, - float16, - float32, - float64, - double, - void_ptr, - Uuid, - Aligned, - align, - Lazy, - ENUM_STRICT, - Padded, - PostPad, - PrePad, -) -from .varint import VarInt, VARINT_LSB, vint -from .compression import ( - Compressed, - ZLibCompressed, - Bz2Compressed, - 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, -) -from .conditional import ( - ConditionalChain, - If, - Else, - ElseIf, - When, - Branch, - End, - Start, - Otherwise, -) +from ._base import DEFAULT_OPTION, INVALID_DEFAULT, Field, singleton +from ._mixin import (Chain, FieldMixin, FieldStruct, Operator, get_args, + get_kwargs) +from .common import (ENUM_STRICT, Aligned, AsLengthRef, Bytes, Computed, Const, + ConstBytes, ConstString, CString, Enum, Int, Lazy, Memory, + Padded, Padding, Pass, PostPad, Prefixed, PrePad, + PyStructFormattedField, String, Timestamp, Transformer, + UInt, Uuid, align, boolean, char, double, float16, + float32, float64, int8, int16, int24, int32, int64, + padding, psize, pssize, uint8, uint16, uint24, uint32, + uint64, void_ptr) +from .compression import (Bz2Compressed, Compressed, LZMACompressed, + LZOCompressed, ZLibCompressed) +from .conditional import (Branch, ConditionalChain, Else, ElseIf, End, If, + Otherwise, Start, When) +from .crypto import And, Encrypted, KeyCipher, Or, Xor +from .digest import (CTX_DIGEST, CTX_DIGEST_ALGO, CTX_DIGEST_HOOK, + CTX_DIGEST_OBJ, HMAC, Adler, Adler_Algo, Adler_Field, + Algorithm, Crc32, Crc32_Algo, Crc32_Field, Digest, + DigestField, DigestFieldAction, HMACAlgorithm, Md5, + Md5_Algo, Md5_Field, Sha1, Sha1_Algo, Sha1_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) from .hook import IOHook -from .digest import ( - Digest, - Algorithm, - Md5, - Sha1, - Sha2_256, - Sha2_224, - Sha2_384, - Sha2_512, - Sha3_224, - Sha3_256, - Sha3_384, - Sha3_512, - Crc32, - Adler, - HMAC, - DigestField, - DigestFieldAction, - Md5_Algo, - Md5_Field, - Sha1_Algo, - Sha1_Field, - Sha2_256_Algo, - Sha2_256_Field, - Sha2_224_Algo, - Sha2_224_Field, - Sha2_384_Algo, - Sha2_384_Field, - Sha2_512_Algo, - Sha2_512_Field, - Sha3_224_Algo, - Sha3_224_Field, - Sha3_256_Algo, - Sha3_256_Field, - Sha3_384_Algo, - Sha3_384_Field, - 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 .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 index 94802fad..486c9a11 100755 --- a/src/caterpillar/fields/_base.py +++ b/src/caterpillar/fields/_base.py @@ -13,48 +13,26 @@ # 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 caterpillar.abc import ( - _StructLike, - _GreedyType, - _PrefixedType, - _IT, - _OT, - _EndianLike, - _ArchLike, - _ContextLike, - _ContextLambda, - _OptionLike, - _ContainsStruct, - _SwitchOptionsT, - _LengthT, - _StreamType, -) -from caterpillar.byteorder import ( - O_DEFAULT_ARCH, - O_DEFAULT_ENDIAN, - LittleEndian, - system_arch, -) -from caterpillar.exception import ( - DynamicSizeError, - StructException, - OptionError, - 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 typing_extensions import Self, TypeVar, override + +from caterpillar import registry +from caterpillar.abc import (_IT, _OT, _ArchLike, _ContainsStruct, + _ContextLambda, _ContextLike, _EndianLike, + _GreedyType, _LengthT, _OptionLike, _PrefixedType, + _StreamType, _StructLike, _SwitchOptionsT) +from caterpillar.byteorder import (O_DEFAULT_ARCH, O_DEFAULT_ENDIAN, + LittleEndian, system_arch) +from caterpillar.context import (CTX_FIELD, CTX_OFFSETS, CTX_SEQ, CTX_STREAM, + CTX_VALUE) +from caterpillar.exception import (DynamicSizeError, InvalidValueError, + OptionError, StructException, + ValidationError) +from caterpillar.options import F_DYNAMIC, GLOBAL_FIELD_FLAGS +from caterpillar.shared import PackMixin, UnpackMixin, getstruct, typeof _T = TypeVar("_T") @@ -250,9 +228,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 diff --git a/src/caterpillar/fields/_mixin.py b/src/caterpillar/fields/_mixin.py index 7d647527..b69c26db 100755 --- a/src/caterpillar/fields/_mixin.py +++ b/src/caterpillar/fields/_mixin.py @@ -13,29 +13,22 @@ # 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._common import WithoutContextVar, pack_seq, unpack_seq +from caterpillar.abc import (_IT, _OT, _ArgType, _ContextLambda, _ContextLike, + _EndianLike, _LengthT, _OptionLike, _StructLike, + _SwitchOptionsT) 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.options import Flag from caterpillar.shared import PackMixin, UnpackMixin, getstruct -from caterpillar.abc import ( - _ContextLambda, - _ContextLike, - _StructLike, - _LengthT, - _IT, - _OT, - _OptionLike, - _EndianLike, - _SwitchOptionsT, - _ArgType, -) + from ._base import Field @@ -68,9 +61,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 +81,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 index ea25ecc1..30548195 100755 --- a/src/caterpillar/fields/common.py +++ b/src/caterpillar/fields/common.py @@ -16,53 +16,31 @@ import datetime import struct as PyStruct import warnings - +from collections.abc import Collection +from enum import Enum as _EnumType +from functools import cached_property from io import BytesIO -from typing import Any, Callable, Generic -from typing_extensions import ( - Buffer, - Final, - Self, - SupportsFloat, - SupportsIndex, - override, - TypeVar, -) from types import NoneType -from functools import cached_property -from enum import Enum as _EnumType +from typing import Any, Callable, Generic from uuid import UUID -from collections.abc import Collection -from caterpillar.abc import ( - _StructLike, - _StreamType, - _ContextLike, - _PrefixedType, - _ContextLambda, - _IT, - _OT, - _LengthT, - _EndianLike, - _GreedyType, -) -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 typing_extensions import (Buffer, Final, Self, SupportsFloat, + SupportsIndex, TypeVar, override) + from caterpillar import registry from caterpillar._common import WithoutContextVar, read_exact +from caterpillar.abc import (_IT, _OT, _ContextLambda, _ContextLike, + _EndianLike, _GreedyType, _LengthT, _PrefixedType, + _StreamType, _StructLike) +from caterpillar.byteorder import (LITTLE_ENDIAN_FMT, O_DEFAULT_ENDIAN, + LittleEndian) +from caterpillar.context import CTX_FIELD, CTX_SEQ, CTX_STREAM +from caterpillar.exception import (DynamicSizeError, InvalidValueError, + ValidationError) +from caterpillar.options import GLOBAL_FIELD_FLAGS, Flag from caterpillar.shared import 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 @@ -1076,9 +1054,7 @@ def unpack_single(self, context: _ContextLike) -> _MemoryOT: if size is Ellipsis: return memoryview(stream.read()) - return memoryview( - read_exact(context, size, "Memory field") - ) # pyright: ignore[reportReturnType] + return memoryview(read_exact(context, size, "Memory field")) # pyright: ignore[reportReturnType] class Bytes(Memory[bytes, bytes]): @@ -1646,7 +1622,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): @@ -2507,9 +2485,7 @@ def __init__( fill: Buffer | int = 0x00, strict: bool = False, ) -> None: - self.struct: _StructLike[_IT, _OT] = ( - getstruct(struct) or struct - ) # pyright: ignore[reportAttributeAccessIssue] + self.struct: _StructLike[_IT, _OT] = getstruct(struct) or struct # pyright: ignore[reportAttributeAccessIssue] self.before: int | _ContextLambda[int] = before self.after: int | _ContextLambda[int] = after fill_bytes = _normalize_fill(fill) diff --git a/src/caterpillar/fields/compression.py b/src/caterpillar/fields/compression.py index 9c1ec17a..7342028d 100755 --- a/src/caterpillar/fields/compression.py +++ b/src/caterpillar/fields/compression.py @@ -14,17 +14,14 @@ # 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 index 3de21686..befb89fd 100755 --- a/src/caterpillar/fields/conditional.py +++ b/src/caterpillar/fields/conditional.py @@ -16,24 +16,19 @@ # 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 typing_extensions import Final, Self, override + +from caterpillar.abc import (_AnnotationT, _ArchLike, _ContextLambda, + _ContextLike, _EndianLike, _OptionLike, + _StructLike) +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 caterpillar.abc import ( - _ArchLike, - _ContextLambda, - _ContextLike, - _EndianLike, - _OptionLike, - _StructLike, - _AnnotationT, -) +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 index 2b47fbfd..868cebab 100755 --- a/src/caterpillar/fields/crypto.py +++ b/src/caterpillar/fields/crypto.py @@ -17,29 +17,20 @@ 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.abc import (_IT, _ArgType, _ContextLambda, _ContextLike, + _GreedyType, _StructLike) from caterpillar.context import CTX_STREAM -from caterpillar.abc import ( - _ContextLambda, - _ContextLike, - _GreedyType, - _StructLike, - _IT, - _ArgType, -) - -from .common import Memory, Bytes +from caterpillar.exception import InvalidValueError, UnsupportedOperation + from ._mixin import get_args, get_kwargs +from .common import Bytes, Memory if TYPE_CHECKING: - from cryptography.hazmat.primitives.ciphers import ( - modes, - CipherAlgorithm, - CipherContext, - ) + from cryptography.hazmat.primitives.ciphers import (CipherAlgorithm, + CipherContext, modes) from cryptography.hazmat.primitives.padding import PaddingContext diff --git a/src/caterpillar/fields/digest.py b/src/caterpillar/fields/digest.py index d2094baf..642ececb 100755 --- 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 index c18e3285..e84cbbb4 100755 --- 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 index 2c8b5158..b2a6b562 100755 --- 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 index 0931f849..532a57c8 100755 --- a/src/caterpillar/fields/pointer.py +++ b/src/caterpillar/fields/pointer.py @@ -14,20 +14,21 @@ # 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, _StreamType, + _StructLike) 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) @@ -54,7 +55,9 @@ def __repr__(self) -> str: def get(self): return self.obj -_PtrT = TypeVar('_PtrT', default=pointer) + +_PtrT = TypeVar("_PtrT", default=pointer) + class Pointer(Generic[_PtrT, _PtrValueT], FieldStruct[int, _PtrT]): """ diff --git a/src/caterpillar/fields/varint.py b/src/caterpillar/fields/varint.py index bc5cdaf4..3ac626f2 100755 --- a/src/caterpillar/fields/varint.py +++ b/src/caterpillar/fields/varint.py @@ -13,22 +13,18 @@ # 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, O_DEFAULT_ENDIAN, + LittleEndian) 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 diff --git a/src/caterpillar/model/__init__.py b/src/caterpillar/model/__init__.py index e10431c7..7b250715 100644 --- a/src/caterpillar/model/__init__.py +++ b/src/caterpillar/model/__init__.py @@ -13,48 +13,16 @@ # 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 ( - Bitfield, - bitfield, - BitfieldEntry, - BitfieldGroup, - BitfieldValueFactory, - issigned, - getbits, - NewGroup, - EndGroup, - SetAlignment, - EnumFactory, - CharFactory, - DEFAULT_ALIGNMENT, - bitfield_factory, - BitfieldDefMixin, -) -from ._template import ( - istemplate, - template, - TemplateTypeVar, - derive, - field_of, - TemplateFieldRef, -) -from .provider import ( - unpack, - unpack_file, - pack, - pack_into, - pack_file, - sizeof, -) +from ._bitfield import (DEFAULT_ALIGNMENT, Bitfield, BitfieldDefMixin, + BitfieldEntry, BitfieldGroup, BitfieldValueFactory, + CharFactory, EndGroup, EnumFactory, NewGroup, + SetAlignment, bitfield, bitfield_factory, getbits, + issigned) +from ._struct import (Invisible, Struct, StructDefMixin, UnionHook, struct, + struct_factory, union) +from ._template import (TemplateFieldRef, TemplateTypeVar, derive, field_of, + 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 index 275addfe..5f3a6122 100755 --- a/src/caterpillar/model/_base.py +++ b/src/caterpillar/model/_base.py @@ -14,51 +14,24 @@ # along with this program. If not, see . # pyright: reportPrivateUsage=false, reportAny=false, reportExplicitAny=false 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 caterpillar.context import ( - CTX_FIELD, - CTX_PATH, - CTX_OBJECT, - CTX_STREAM, - CTX_SEQ, - O_CONTEXT_FACTORY, - Context, - CTX_ROOT, -) -from caterpillar.exception import StructException, ValidationError -from caterpillar.options import ( - S_DISCARD_CONST, - S_DISCARD_UNNAMED, - S_UNION, - S_REPLACE_TYPES, -) -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 typing_extensions import Self, TypeVar, override + from caterpillar import registry -from caterpillar.abc import ( - _StructLike, - _ContextLike, - _OptionLike, - _ContextLambda, - _EndianLike, - _ArchLike, - _StreamType, - _ActionLike, -) +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_OBJECT, CTX_PATH, CTX_ROOT, + CTX_SEQ, CTX_STREAM, O_CONTEXT_FACTORY, + Context) +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_REPLACE_TYPES, S_UNION) +from caterpillar.shared import ATTR_ACTION_PACK, ATTR_ACTION_UNPACK, Action class _Member: diff --git a/src/caterpillar/model/_bitfield.py b/src/caterpillar/model/_bitfield.py index c97a82b2..a5139b82 100755 --- a/src/caterpillar/model/_bitfield.py +++ b/src/caterpillar/model/_bitfield.py @@ -15,70 +15,30 @@ # 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, - 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.byteorder import ( - LITTLE_ENDIAN_FMT, - O_DEFAULT_ENDIAN, - LittleEndian, -) -from caterpillar.options import ( - B_GROUP_NEW, - 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 typing_extensions import (ClassVar, Self, TypeVar, dataclass_transform, + overload, override) + +from caterpillar.abc import (_ActionLike, _ArchLike, _ContextLike, _EndianLike, + _OptionLike, _StructLike) +from caterpillar.byteorder import (LITTLE_ENDIAN_FMT, O_DEFAULT_ENDIAN, + LittleEndian) +from caterpillar.context import (CTX_FIELD, CTX_OBJECT, CTX_PATH, CTX_STREAM, + O_CONTEXT_FACTORY, Context) 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.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, Flag) +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") diff --git a/src/caterpillar/model/_struct.py b/src/caterpillar/model/_struct.py index 6ea758e0..888bf19d 100755 --- a/src/caterpillar/model/_struct.py +++ b/src/caterpillar/model/_struct.py @@ -13,47 +13,29 @@ # 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_extensions import ( - ClassVar, - Self, - dataclass_transform, - override, - overload, - Buffer, -) +from typing import Any, Callable, Generic, Literal, ParamSpec, TypeVar + +from typing_extensions import (Buffer, ClassVar, Self, dataclass_transform, + overload, 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, - _ContextLike, - _StructLike, - _OptionLike, - _EndianLike, - _ArchLike, - _LengthT, -) -from .provider import unpack, pack, unpack_file, pack_into, sizeof -from ._base import Sequence +from caterpillar.abc import (_ArchLike, _ContextLike, _EndianLike, _LengthT, + _OptionLike, _StreamType, _StructLike) +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") diff --git a/src/caterpillar/model/_template.py b/src/caterpillar/model/_template.py index 81959102..23f79efe 100755 --- a/src/caterpillar/model/_template.py +++ b/src/caterpillar/model/_template.py @@ -14,38 +14,23 @@ # # 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 caterpillar.fields import Field, INVALID_DEFAULT +from typing import (Annotated, Any, Callable, Generic, TypeVar, get_args, + get_origin) + +from typing_extensions import dataclass_transform, overload, override + +from caterpillar.abc import (_ArchLike, _ContextLambda, _EndianLike, + _GreedyType, _LengthT, _StructLike, _SwitchLambda) +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 -from caterpillar.abc import ( - _LengthT, - _StructLike, - _ContextLambda, - _GreedyType, - _SwitchLambda, - _EndianLike, - _ArchLike, -) - _TYPEVAR_TYPE: type[TypeVar] = type(TypeVar("_CaterpillarTemplateTypeVar")) diff --git a/src/caterpillar/model/provider.py b/src/caterpillar/model/provider.py index ba72762c..fc6c5e04 100644 --- a/src/caterpillar/model/provider.py +++ b/src/caterpillar/model/provider.py @@ -13,37 +13,23 @@ # 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 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 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.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.shared import (ATTR_PACK, MODE_PACK, MODE_UNPACK, getstruct, + hasstruct) @overload diff --git a/src/caterpillar/options.py b/src/caterpillar/options.py index 9df0bc30..07e3b2aa 100755 --- 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 @dataclass(init=False, eq=False) diff --git a/src/caterpillar/py.py b/src/caterpillar/py.py index fade08f6..a1200c71 100644 --- a/src/caterpillar/py.py +++ b/src/caterpillar/py.py @@ -14,126 +14,39 @@ # 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 .byteorder import ( - ByteOrder, - SysNative, - Dynamic, - DynByteOrder, - Arch, - system_arch, - Native, - BigEndian, - LittleEndian, - NetEndian, - AARCH64, - ARM, - ARM64, - MIPS, - MIPS64, - PowerPC, - PowerPC64, - RISC_V, - RISC_V64, - SPARC, - SPARC64, - 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_FIELD, - CTX_VALUE, - CTX_POS, - CTX_INDEX, - CTX_PATH, - CTX_SEQ, - CTX_ARCH, - CTX_ROOT, - CTX_ORDER, - ExprMixin, - root, - SetContextVar, - O_CONTEXT_FACTORY, - parentctx, -) -from .exception import ( - StructException, - StreamError, - DelegationError, - InvalidValueError, - Stop, - OptionError, - ValidationError, - DynamicSizeError, -) +from ._common import WithoutContextVar, iseof, pack_seq, unpack_seq +from .byteorder import (AARCH64, AMD, AMD64, ARM, ARM64, MIPS, MIPS64, RISC_V, + RISC_V64, SPARC, SPARC64, Arch, BigEndian, ByteOrder, + Dynamic, DynByteOrder, LittleEndian, Native, NetEndian, + PowerPC, PowerPC64, SysNative, system_arch, x86, + x86_64) +from .context import (CTX_ARCH, CTX_FIELD, CTX_INDEX, CTX_OBJECT, CTX_OFFSETS, + CTX_ORDER, CTX_PARENT, CTX_PATH, CTX_POS, CTX_ROOT, + CTX_SEQ, CTX_STREAM, CTX_VALUE, O_CONTEXT_FACTORY, + BinaryExpression, ConditionContext, Context, + ContextLength, ContextPath, ExprMixin, SetContextVar, + UnaryExpression, ctx, parent, parentctx, root, this) +from .exception import (DelegationError, DynamicSizeError, InvalidValueError, + OptionError, Stop, StreamError, StructException, + ValidationError) from .fields import * # noqa from .model import * # noqa -from .options import ( - Flag, - F_SEQUENTIAL, - F_DYNAMIC, - F_KEEP_POSITION, - F_OFFSET_OVERRIDE, - set_field_flags, - set_struct_flags, - set_union_flags, - S_ADD_BYTES, - S_DISCARD_CONST, - S_SLOTS, - S_DISCARD_UNNAMED, - S_REPLACE_TYPES, - S_UNION, - S_EVAL_ANNOTATIONS, - GLOBAL_BITFIELD_FLAGS, - GLOBAL_STRUCT_OPTIONS, - GLOBAL_UNION_OPTIONS, - GLOBAL_FIELD_FLAGS, - 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, -) -from ._common import WithoutContextVar, iseof, pack_seq, unpack_seq -from .shared import ( - ATTR_ACTION_PACK, - ATTR_STRUCT, - Action, - ATTR_ACTION_UNPACK, - ATTR_BITS, - ATTR_BYTEORDER, - ATTR_SIGNED, - ATTR_TEMPLATE, - ATTR_TYPE, - getstruct, - hasstruct, - MODE_PACK, - MODE_UNPACK, - typeof, - constval, - ATTR_PACK, - ATTR_UNPACK, -) +from .options import (B_GROUP_END, B_GROUP_KEEP, B_GROUP_NEW, B_NO_AUTO_BOOL, + B_OVERWRITE_ALIGNMENT, F_DYNAMIC, F_KEEP_POSITION, + F_OFFSET_OVERRIDE, F_SEQUENTIAL, GLOBAL_BITFIELD_FLAGS, + GLOBAL_FIELD_FLAGS, GLOBAL_STRUCT_OPTIONS, + GLOBAL_UNION_OPTIONS, O_ARRAY_FACTORY, S_ADD_BYTES, + S_DISCARD_CONST, S_DISCARD_UNNAMED, S_EVAL_ANNOTATIONS, + S_REPLACE_TYPES, S_SLOTS, S_UNION, Flag, get_flag, + get_flags, has_flag, set_field_flags, set_struct_flags, + set_union_flags) +# use this module to import everything Python related +from .registry import TypeConverter, annotation_registry, to_struct +from .shared import (ATTR_ACTION_PACK, ATTR_ACTION_UNPACK, ATTR_BITS, + ATTR_BYTEORDER, ATTR_PACK, ATTR_SIGNED, ATTR_STRUCT, + ATTR_TEMPLATE, ATTR_TYPE, ATTR_UNPACK, MODE_PACK, + MODE_UNPACK, Action, constval, getstruct, hasstruct, + typeof) __all__ = [ "AARCH64", 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..7bd8a07c 100644 --- a/src/caterpillar/shared.py +++ b/src/caterpillar/shared.py @@ -42,21 +42,13 @@ 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 caterpillar.abc import ( - _ContextLambda, - _StructLike, - _IT, - _OT, - _SupportsType, - _ContainsStruct, - _ActionLike, - _ArchLike, - _EndianLike, - _StreamType, -) +from typing import TYPE_CHECKING, Any, Generic, overload + +from typing_extensions import Buffer, Final, Literal, TypeIs, override + +from caterpillar.abc import (_IT, _OT, _ActionLike, _ArchLike, _ContainsStruct, + _ContextLambda, _EndianLike, _StreamType, + _StructLike, _SupportsType) if TYPE_CHECKING: from caterpillar.fields import FieldStruct @@ -416,7 +408,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 +514,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..1783130f 100644 --- a/src/caterpillar/shortcuts.py +++ b/src/caterpillar/shortcuts.py @@ -13,40 +13,25 @@ # 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 .byteorder import ( - AARCH64, - AMD, - AMD64, - ARM, - ARM64, - BigEndian, - LittleEndian, - Dynamic, - 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 .model import ( - bitfield, - pack, - pack_file, - pack_into, - 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 .byteorder import (AARCH64, AMD, AMD64, ARM, ARM64, RISC_V, RISC_V64, + BigEndian, Dynamic, LittleEndian, PowerPC, PowerPC64, + x86, x86_64) +from .context import ContextLength as lenof +from .context import ContextPath +from .context import ctx +from .context import ctx as C +from .context import parent +from .context import parent as P +from .context import root as G +from .context import this 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) +from .registry import to_struct +from .shared import getstruct, hasstruct, typeof __all__ = [ "AARCH64", diff --git a/src/caterpillar/types.py b/src/caterpillar/types.py index 4a20b0f5..f1ca8069 100644 --- a/src/caterpillar/types.py +++ b/src/caterpillar/types.py @@ -12,37 +12,16 @@ # # 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 ( - boolean, - char, - uint8, - uint16, - uint24, - uint32, - uint64, - int8, - int16, - int24, - int32, - int64, - float16, - float32, - float64, - double, - psize, - pssize, - Uuid, - Pass, - CString, - Timestamp, - void_ptr, -) -from .fields.varint import vint +from .fields.common import (CString, Pass, Timestamp, Uuid, boolean, char, + double, float16, float32, float64, int8, int16, + int24, int32, int64, psize, pssize, uint8, uint16, + uint24, uint32, uint64, void_ptr) 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..1db8c995 100644 --- a/src/code_gen/genapi.py +++ b/src/code_gen/genapi.py @@ -3,18 +3,13 @@ ===================================================== """ -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 +45,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..b8e5b9a3 100644 --- a/test/_C/test_protocols.py +++ b/test/_C/test_protocols.py @@ -1,28 +1,14 @@ # pyright: reportPrivateUsage=false import pytest + import caterpillar if caterpillar.native_support(): - 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, - ) - + from caterpillar.abc import (_ArchLike, _ContextLike, _EndianLike, + _OptionLike, _StructLike) + from caterpillar.c import (AtOffset, Atom, BuiltinAtom, Conditional, + Repeated, Switch, c_Arch, c_Context, c_Endian, + c_Option) # === PROTOCOL COMPLIANCE === # All tests below should verify the protocol compliance of all classes @@ -48,6 +34,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..61a09a4a 100644 --- a/test/_Py/fields/test_py_aligned.py +++ b/test/_Py/fields/test_py_aligned.py @@ -2,7 +2,8 @@ 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_bytes.py b/test/_Py/fields/test_py_bytes.py index 66b92380..479f9409 100644 --- a/test/_Py/fields/test_py_bytes.py +++ b/test/_Py/fields/test_py_bytes.py @@ -2,7 +2,8 @@ import pytest -from caterpillar.py import Bytes, ValidationError, f, pack, struct, this, unpack +from caterpillar.py import (Bytes, ValidationError, f, pack, struct, this, + 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..706c4084 100644 --- a/test/_Py/fields/test_py_chain.py +++ b/test/_Py/fields/test_py_chain.py @@ -1,4 +1,5 @@ -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..3ae9fbc3 100644 --- a/test/_Py/fields/test_py_compressed.py +++ b/test/_Py/fields/test_py_compressed.py @@ -1,17 +1,12 @@ -from io import BytesIO - import lzma import zlib +from io import BytesIO import pytest +from caterpillar.fields.compression import (Bz2Compressed, Compressed, + LZMACompressed, ZLibCompressed) from caterpillar.py import Bytes, pack, root, unpack -from caterpillar.fields.compression import ( - Bz2Compressed, - Compressed, - LZMACompressed, - ZLibCompressed, -) @pytest.mark.parametrize("payload", [b"", b"hello hello hello", bytes(range(256))]) diff --git a/test/_Py/fields/test_py_computed.py b/test/_Py/fields/test_py_computed.py index de0dba82..7b45dab8 100644 --- a/test/_Py/fields/test_py_computed.py +++ b/test/_Py/fields/test_py_computed.py @@ -1,6 +1,7 @@ import io -from caterpillar.py import Bytes, Computed, Context, f, pack, struct, this, unpack +from caterpillar.py import (Bytes, Computed, Context, f, pack, struct, this, + unpack) from caterpillar.types import uint8_t diff --git a/test/_Py/fields/test_py_const.py b/test/_Py/fields/test_py_const.py index 6dddc18e..6bbb58c0 100644 --- a/test/_Py/fields/test_py_const.py +++ b/test/_Py/fields/test_py_const.py @@ -1,7 +1,8 @@ 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..ff836fad 100644 --- a/test/_Py/fields/test_py_digest.py +++ b/test/_Py/fields/test_py_digest.py @@ -1,18 +1,12 @@ +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_Field, - Sha2_256, - Md5_Algo, - Sha2_256_Algo, - Sha2_256_Field, -) +from caterpillar.fields.digest import (DigestField, Md5, Md5_Algo, Md5_Field, + Sha2_256, 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_enum.py b/test/_Py/fields/test_py_enum.py index c0778b7f..6ebcf4b7 100644 --- a/test/_Py/fields/test_py_enum.py +++ b/test/_Py/fields/test_py_enum.py @@ -2,7 +2,8 @@ import pytest -from caterpillar.py import Enum, InvalidValueError, ValidationError, pack, uint8, unpack +from caterpillar.py import (Enum, InvalidValueError, ValidationError, pack, + uint8, unpack) class Color(enum.Enum): diff --git a/test/_Py/fields/test_py_if.py b/test/_Py/fields/test_py_if.py index db618b4d..fdf3108b 100644 --- a/test/_Py/fields/test_py_if.py +++ b/test/_Py/fields/test_py_if.py @@ -3,27 +3,10 @@ import pytest -from caterpillar.py import ( - BigEndian, - Branch, - Else, - ElseIf, - End, - If, - Invisible, - LittleEndian, - Start, - StructException, - When, - Otherwise, - f, - pack, - struct, - this, - uint8, - uint16, - unpack, -) +from caterpillar.py import (BigEndian, Branch, Else, ElseIf, End, If, + Invisible, LittleEndian, Otherwise, Start, + StructException, When, f, pack, struct, this, + uint8, uint16, unpack) # <3.14 SYNTAX: diff --git a/test/_Py/fields/test_py_int.py b/test/_Py/fields/test_py_int.py index d6d24787..071fa762 100644 --- a/test/_Py/fields/test_py_int.py +++ b/test/_Py/fields/test_py_int.py @@ -1,6 +1,7 @@ import pytest -from caterpillar.py import BigEndian, Int, LittleEndian, ValidationError, pack, unpack +from caterpillar.py import (BigEndian, Int, LittleEndian, ValidationError, + pack, unpack) # --------------------------------------------------------------------------- # 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_lengthfor.py b/test/_Py/fields/test_py_lengthfor.py index 1fc57ad2..ce6e8c10 100644 --- a/test/_Py/fields/test_py_lengthfor.py +++ b/test/_Py/fields/test_py_lengthfor.py @@ -2,19 +2,8 @@ import pytest -from caterpillar.py import ( - AsLengthRef, - BigEndian, - Bytes, - String, - f, - pack, - struct, - this, - uint8, - uint16, - unpack, -) +from caterpillar.py import (AsLengthRef, BigEndian, Bytes, String, f, pack, + struct, this, uint8, uint16, unpack) # --------------------------------------------------------------------------- # 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..0ac25b11 100644 --- a/test/_Py/fields/test_py_memory.py +++ b/test/_Py/fields/test_py_memory.py @@ -1,14 +1,7 @@ import pytest -from caterpillar.py import ( - DynamicSizeError, - Memory, - pack, - root, - sizeof, - unpack, - ValidationError, -) +from caterpillar.py import (DynamicSizeError, Memory, ValidationError, pack, + root, sizeof, unpack) def test_py_memory_unpack(): 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..1c08b5cd 100644 --- a/test/_Py/fields/test_py_padding.py +++ b/test/_Py/fields/test_py_padding.py @@ -2,24 +2,9 @@ import pytest -from caterpillar.py import ( - DynamicSizeError, - Padded, - PostPad, - PrePad, - ValidationError, - constval, - f, - padding, - pack, - sizeof, - struct, - this, - uint8, - uint16, - unpack, - Padding, -) +from caterpillar.py import (DynamicSizeError, Padded, Padding, PostPad, PrePad, + ValidationError, constval, f, pack, padding, + sizeof, struct, this, uint8, uint16, unpack) 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..8cbdbbd7 100644 --- a/test/_Py/fields/test_py_pointer.py +++ b/test/_Py/fields/test_py_pointer.py @@ -2,7 +2,8 @@ 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_prefixed.py b/test/_Py/fields/test_py_prefixed.py index 77ce7d2b..a5a1212b 100644 --- a/test/_Py/fields/test_py_prefixed.py +++ b/test/_Py/fields/test_py_prefixed.py @@ -2,16 +2,8 @@ import pytest -from caterpillar.py import ( - BigEndian, - Bytes, - Prefixed, - ValidationError, - pack, - uint8, - uint16, - unpack, -) +from caterpillar.py import (BigEndian, Bytes, Prefixed, ValidationError, pack, + uint8, uint16, unpack) def test_prefixed_pack_unpack(): diff --git a/test/_Py/fields/test_py_pystruct.py b/test/_Py/fields/test_py_pystruct.py index c50db97f..e019cc2c 100644 --- a/test/_Py/fields/test_py_pystruct.py +++ b/test/_Py/fields/test_py_pystruct.py @@ -2,30 +2,10 @@ import pytest -from caterpillar.py import ( - BigEndian, - Context, - LittleEndian, - SysNative, - boolean, - char, - float16, - float32, - float64, - int8, - int16, - int32, - int64, - pack, - psize, - pssize, - uint8, - uint16, - uint32, - uint64, - unpack, - void_ptr, -) +from caterpillar.py import (BigEndian, Context, LittleEndian, SysNative, + boolean, char, float16, float32, float64, int8, + int16, int32, int64, pack, psize, pssize, uint8, + uint16, uint32, uint64, unpack, void_ptr) def test_integer_formats_sizes_and_endianness(): @@ -75,22 +55,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 +98,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..0d3925ae 100644 --- a/test/_Py/model/test_bitfield.py +++ b/test/_Py/model/test_bitfield.py @@ -2,33 +2,15 @@ import pytest -from caterpillar.model import ( - Bitfield, - bitfield, - EndGroup, - SetAlignment, - sizeof, - unpack, - CharFactory, - pack, - Invisible -) -from caterpillar.options import ( - S_REPLACE_TYPES, -) -from caterpillar.fields import uint16, Bytes, uint8 +from caterpillar.fields import Bytes, uint8, uint16 +from caterpillar.model import (Bitfield, CharFactory, EndGroup, Invisible, + SetAlignment, bitfield, pack, sizeof, unpack) +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 -from caterpillar.types import ( - balign_t, - int1_t, - int2_t, - int3_t, - uint16_t, - uint24_t, - uint32_t, -) +from caterpillar.types import (balign_t, int1_t, int2_t, int3_t, uint16_t, + uint24_t, uint32_t) def test_bitfield_syntax__standard(): diff --git a/test/_Py/model/test_template.py b/test/_Py/model/test_template.py index 1d1398f0..9f3b4deb 100644 --- a/test/_Py/model/test_template.py +++ b/test/_Py/model/test_template.py @@ -1,17 +1,7 @@ -from typing_extensions import TypeVar, Generic - -from caterpillar.py import ( - BigEndian, - derive, - f, - field_of, - struct, - template, - uint16, - uint8, - pack, - unpack, -) +from typing_extensions import Generic, TypeVar + +from caterpillar.py import (BigEndian, derive, f, field_of, pack, struct, + template, uint8, uint16, unpack) from caterpillar.types import uint8_t diff --git a/test/_Py/test_arch.py b/test/_Py/test_arch.py index f845b569..b6581f81 100644 --- a/test/_Py/test_arch.py +++ b/test/_Py/test_arch.py @@ -1,20 +1,7 @@ import sys -from caterpillar.py import ( - Arch, - Field, - f, - intptr, - pack, - sizeof, - struct, - system_arch, - uint8, - unpack, - uintptr, - x86, - x86_64, -) +from caterpillar.py import (Arch, Field, f, intptr, pack, sizeof, struct, + system_arch, uint8, uintptr, unpack, x86, x86_64) # --------------------------------------------------------------------------- # diff --git a/test/_Py/test_byteorder.py b/test/_Py/test_byteorder.py index 7274e5b9..644c8cbd 100644 --- a/test/_Py/test_byteorder.py +++ b/test/_Py/test_byteorder.py @@ -1,23 +1,9 @@ -from caterpillar.py import ( - Invisible, - struct, - pack, - uint64, - unpack, - uint16, - uint32, - Dynamic, - DynByteOrder, - BigEndian, - LittleEndian, - CTX_ORDER, - ctx, - this, - f, -) -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.context import SetContextVar +from caterpillar.py import (CTX_ORDER, BigEndian, Dynamic, DynByteOrder, + Invisible, LittleEndian, ctx, f, pack, struct, + this, uint16, uint32, uint64, unpack) +from caterpillar.types import uint8_t, uint16_t, uint32_t, uint64_t def test_byteorder_pack_explicit(): @@ -201,5 +187,3 @@ class Format: obj = Format(a=0x1234, b=0x56789ABC) data = pack(obj) assert data.hex() == "1234bc9a7856" - - 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..96b33cc6 100644 --- a/test/_Py/test_flag.py +++ b/test/_Py/test_flag.py @@ -1,10 +1,5 @@ -from caterpillar.options import ( - Flag, - configure, - GLOBAL_STRUCT_OPTIONS, - get_flag, - get_flags, -) +from caterpillar.options import (GLOBAL_STRUCT_OPTIONS, Flag, configure, + get_flag, get_flags) from caterpillar.py import Context diff --git a/test/_Py/test_protocols.py b/test/_Py/test_protocols.py index 2fb250c0..870b057c 100644 --- a/test/_Py/test_protocols.py +++ b/test/_Py/test_protocols.py @@ -1,61 +1,19 @@ # pyright: reportPrivateUsage=false import pytest -from caterpillar.py import ( - BinaryExpression, - Bitfield, - Chain, - Compressed, - ConditionalChain, - ConstBytes, - ConstString, - ContextLength, - ContextPath, - DigestField, - DigestFieldAction, - Encrypted, - Enum, - Field, - IPv4Address, - IPv6Address, - Int, - KeyCipher, - Lazy, - MACAddress, - Pointer, - PyStructFormattedField, - RelativePointer, - Sequence, - Struct, - SysNative, - Const, - Dynamic, - Transformer, - UInt, - Flag, - UnaryExpression, - VarInt, - system_arch, - Context, - String, - Bytes, - Memory, - Computed, - Pass, - CString, - Prefixed, - Uuid, - Aligned, -) -from caterpillar.abc import ( - _StructLike, - _OptionLike, - _EndianLike, - _ArchLike, - _ContextLike, - _ContextLambda, - _ActionLike, -) +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, KeyCipher, + Lazy, MACAddress, Memory, Pass, Pointer, Prefixed, + PyStructFormattedField, RelativePointer, Sequence, + String, Struct, SysNative, Transformer, UInt, + UnaryExpression, Uuid, VarInt, system_arch) # === PROTOCOL COMPLIANCE === @@ -119,6 +77,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 From 25716aac5d6662be03d4cdbce950fca29e10d679 Mon Sep 17 00:00:00 2001 From: MatrixEditor <58256046+MatrixEditor@users.noreply.github.com> Date: Sat, 1 Aug 2026 09:22:37 +0200 Subject: [PATCH 3/8] fix(context): .parent resolves correctly to the parent object --- src/caterpillar/context.py | 46 ++++++++++++++++++++++++++++---------- 1 file changed, 34 insertions(+), 12 deletions(-) diff --git a/src/caterpillar/context.py b/src/caterpillar/context.py index 551ab559..fa29abbb 100755 --- a/src/caterpillar/context.py +++ b/src/caterpillar/context.py @@ -21,15 +21,29 @@ import warnings from dataclasses import dataclass from types import FrameType, TracebackType -from typing import (Annotated, Any, Callable, Generic, Protocol, get_args, - get_origin) - -from typing_extensions import (Buffer, Final, Literal, Self, Sized, TypeVar, - overload, override) +from typing import Annotated, Any, Callable, Generic, Protocol, get_args, get_origin + +from typing_extensions import ( + Buffer, + Final, + Literal, + Self, + Sized, + TypeVar, + overload, + override, +) -from caterpillar.abc import (_IT, _OT, _ArchLike, _ContextFactoryLike, - _ContextLambda, _ContextLike, _EndianLike, - _StreamType) +from caterpillar.abc import ( + _IT, + _OT, + _ArchLike, + _ContextFactoryLike, + _ContextLambda, + _ContextLike, + _EndianLike, + _StreamType, +) from caterpillar.exception import StructException from caterpillar.options import Flag from caterpillar.registry import to_struct @@ -712,10 +726,18 @@ def __str__(self) -> str: @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])) + """ + .. 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]: From 3e48066c537d5ed116903c51adff97a43f9eb2a8 Mon Sep 17 00:00:00 2001 From: MatrixEditor <58256046+MatrixEditor@users.noreply.github.com> Date: Sat, 1 Aug 2026 09:25:53 +0200 Subject: [PATCH 4/8] refactor: update file imports --- src/caterpillar/_C.pyi | 14 ++- src/caterpillar/_common.py | 32 ++++-- src/caterpillar/abc.py | 11 +- src/caterpillar/byteorder.py | 10 +- src/caterpillar/context.py | 21 ++-- src/caterpillar/fields/__init__.py | 156 +++++++++++++++++++++----- src/caterpillar/fields/_base.py | 41 +++++-- src/caterpillar/fields/_mixin.py | 15 ++- src/caterpillar/fields/common.py | 32 ++++-- src/caterpillar/fields/compression.py | 3 +- src/caterpillar/fields/conditional.py | 15 ++- src/caterpillar/fields/crypto.py | 17 ++- src/caterpillar/fields/digest.py | 0 src/caterpillar/fields/hook.py | 0 src/caterpillar/fields/net.py | 0 src/caterpillar/fields/pointer.py | 19 +++- src/caterpillar/fields/varint.py | 6 +- src/caterpillar/model/__init__.py | 43 +++++-- src/caterpillar/model/_base.py | 34 ++++-- src/caterpillar/model/_bitfield.py | 59 +++++++--- src/caterpillar/model/_struct.py | 32 ++++-- src/caterpillar/model/_template.py | 14 ++- src/caterpillar/model/provider.py | 25 +++-- src/caterpillar/options.py | 0 src/caterpillar/py.py | 144 +++++++++++++++++++----- src/caterpillar/shared.py | 15 ++- src/caterpillar/shortcuts.py | 32 +++++- src/caterpillar/types.py | 29 ++++- src/code_gen/genapi.py | 3 +- test/_C/test_protocols.py | 24 +++- test/_Py/fields/test_py_aligned.py | 14 ++- test/_Py/fields/test_py_bytes.py | 3 +- test/_Py/fields/test_py_chain.py | 3 +- test/_Py/fields/test_py_compressed.py | 8 +- test/_Py/fields/test_py_computed.py | 3 +- test/_Py/fields/test_py_const.py | 3 +- test/_Py/fields/test_py_digest.py | 11 +- test/_Py/fields/test_py_enum.py | 3 +- test/_Py/fields/test_py_if.py | 25 ++++- test/_Py/fields/test_py_int.py | 3 +- test/_Py/fields/test_py_lengthfor.py | 15 ++- test/_Py/fields/test_py_memory.py | 11 +- test/_Py/fields/test_py_padding.py | 21 +++- test/_Py/fields/test_py_pointer.py | 11 +- test/_Py/fields/test_py_prefixed.py | 12 +- test/_Py/fields/test_py_pystruct.py | 28 ++++- test/_Py/model/test_bitfield.py | 24 +++- test/_Py/model/test_template.py | 14 ++- test/_Py/test_arch.py | 17 ++- test/_Py/test_byteorder.py | 20 +++- test/_Py/test_flag.py | 9 +- test/_Py/test_protocols.py | 68 ++++++++--- 52 files changed, 917 insertions(+), 255 deletions(-) mode change 100755 => 100644 src/caterpillar/_common.py mode change 100755 => 100644 src/caterpillar/abc.py mode change 100755 => 100644 src/caterpillar/byteorder.py mode change 100755 => 100644 src/caterpillar/context.py mode change 100755 => 100644 src/caterpillar/fields/_base.py mode change 100755 => 100644 src/caterpillar/fields/_mixin.py mode change 100755 => 100644 src/caterpillar/fields/common.py mode change 100755 => 100644 src/caterpillar/fields/compression.py mode change 100755 => 100644 src/caterpillar/fields/conditional.py mode change 100755 => 100644 src/caterpillar/fields/crypto.py mode change 100755 => 100644 src/caterpillar/fields/digest.py mode change 100755 => 100644 src/caterpillar/fields/hook.py mode change 100755 => 100644 src/caterpillar/fields/net.py mode change 100755 => 100644 src/caterpillar/fields/pointer.py mode change 100755 => 100644 src/caterpillar/fields/varint.py mode change 100755 => 100644 src/caterpillar/model/_base.py mode change 100755 => 100644 src/caterpillar/model/_bitfield.py mode change 100755 => 100644 src/caterpillar/model/_struct.py mode change 100755 => 100644 src/caterpillar/model/_template.py mode change 100755 => 100644 src/caterpillar/options.py diff --git a/src/caterpillar/_C.pyi b/src/caterpillar/_C.pyi index 3af33b49..3e20dc0e 100644 --- a/src/caterpillar/_C.pyi +++ b/src/caterpillar/_C.pyi @@ -19,9 +19,17 @@ from typing import Any, Generic from typing_extensions import Self, override from caterpillar import native_support -from caterpillar.abc import (_IT, _OT, _ContextLambda, _ContextLike, - _EndianLike, _LengthT, _StructLike, - _SupportsSetEndian, _SwitchOptionsT) +from caterpillar.abc import ( + _IT, + _OT, + _ContextLambda, + _ContextLike, + _EndianLike, + _LengthT, + _StructLike, + _SupportsSetEndian, + _SwitchOptionsT, +) if native_support(): BIG_ENDIAN: c_Endian diff --git a/src/caterpillar/_common.py b/src/caterpillar/_common.py old mode 100755 new mode 100644 index de9b8bd0..d9cfe20f --- a/src/caterpillar/_common.py +++ b/src/caterpillar/_common.py @@ -18,13 +18,31 @@ from types import TracebackType from typing import TYPE_CHECKING, Any, Callable -from caterpillar.abc import (_IT, _OT, _ContextLike, _LengthT, _PrefixedType, - _StreamType, _SupportsPack) -from caterpillar.context import (CTX_FIELD, CTX_INDEX, CTX_OBJECT, CTX_PATH, - CTX_SEQ, CTX_STREAM, O_CONTEXT_FACTORY, - Context) -from caterpillar.exception import (InvalidValueError, Stop, StructException, - ValidationError) +from caterpillar.abc import ( + _IT, + _OT, + _ContextLike, + _LengthT, + _PrefixedType, + _StreamType, + _SupportsPack, +) +from caterpillar.context import ( + CTX_FIELD, + CTX_INDEX, + CTX_OBJECT, + CTX_PATH, + CTX_SEQ, + CTX_STREAM, + O_CONTEXT_FACTORY, + Context, +) +from caterpillar.exception import ( + InvalidValueError, + Stop, + StructException, + ValidationError, +) from caterpillar.options import O_ARRAY_FACTORY if TYPE_CHECKING: diff --git a/src/caterpillar/abc.py b/src/caterpillar/abc.py old mode 100755 new mode 100644 index 6249bf5c..7a755c2b --- a/src/caterpillar/abc.py +++ b/src/caterpillar/abc.py @@ -16,8 +16,15 @@ from collections.abc import Callable, Collection, Iterable from io import IOBase from types import EllipsisType, NoneType -from typing import (TYPE_CHECKING, Any, Final, Literal, Protocol, TypeAlias, - runtime_checkable) +from typing import ( + TYPE_CHECKING, + Any, + Final, + Literal, + Protocol, + TypeAlias, + runtime_checkable, +) from typing_extensions import Buffer, TypeVar, overload, override diff --git a/src/caterpillar/byteorder.py b/src/caterpillar/byteorder.py old mode 100755 new mode 100644 index f28e392b..601e04ae --- a/src/caterpillar/byteorder.py +++ b/src/caterpillar/byteorder.py @@ -23,8 +23,14 @@ from typing_extensions import override -from caterpillar.abc import (_OT, ArchLike, ContextLambda, ContextLike, - EndianLike, SupportsSetEndian) +from caterpillar.abc import ( + _OT, + ArchLike, + ContextLambda, + ContextLike, + EndianLike, + SupportsSetEndian, +) from caterpillar.context import CTX_ORDER from caterpillar.options import Flag from caterpillar.shared import ATTR_BYTEORDER diff --git a/src/caterpillar/context.py b/src/caterpillar/context.py old mode 100755 new mode 100644 index fa29abbb..d21e08fc --- a/src/caterpillar/context.py +++ b/src/caterpillar/context.py @@ -13,27 +13,26 @@ # 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 collections.abc import Callable, Sized from dataclasses import dataclass from types import FrameType, TracebackType -from typing import Annotated, Any, Callable, Generic, Protocol, get_args, get_origin - -from typing_extensions import ( - Buffer, +from typing import ( + Annotated, + Any, Final, + Generic, Literal, - Self, - Sized, - TypeVar, - overload, - override, + Protocol, + get_args, + get_origin, ) +from typing_extensions import Buffer, Self, TypeVar, overload, override + from caterpillar.abc import ( _IT, _OT, diff --git a/src/caterpillar/fields/__init__.py b/src/caterpillar/fields/__init__.py index 9db8f7c7..572caa4f 100644 --- a/src/caterpillar/fields/__init__.py +++ b/src/caterpillar/fields/__init__.py @@ -13,37 +13,137 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . from ._base import DEFAULT_OPTION, INVALID_DEFAULT, Field, singleton -from ._mixin import (Chain, FieldMixin, FieldStruct, Operator, get_args, - get_kwargs) -from .common import (ENUM_STRICT, Aligned, AsLengthRef, Bytes, Computed, Const, - ConstBytes, ConstString, CString, Enum, Int, Lazy, Memory, - Padded, Padding, Pass, PostPad, Prefixed, PrePad, - PyStructFormattedField, String, Timestamp, Transformer, - UInt, Uuid, align, boolean, char, double, float16, - float32, float64, int8, int16, int24, int32, int64, - padding, psize, pssize, uint8, uint16, uint24, uint32, - uint64, void_ptr) -from .compression import (Bz2Compressed, Compressed, LZMACompressed, - LZOCompressed, ZLibCompressed) -from .conditional import (Branch, ConditionalChain, Else, ElseIf, End, If, - Otherwise, Start, When) +from ._mixin import Chain, FieldMixin, FieldStruct, Operator, get_args, get_kwargs +from .common import ( + ENUM_STRICT, + Aligned, + AsLengthRef, + Bytes, + Computed, + Const, + ConstBytes, + ConstString, + CString, + Enum, + Int, + Lazy, + Memory, + Padded, + Padding, + Pass, + PostPad, + Prefixed, + PrePad, + PyStructFormattedField, + String, + Timestamp, + Transformer, + UInt, + Uuid, + align, + boolean, + char, + double, + float16, + float32, + float64, + int8, + int16, + int24, + int32, + int64, + padding, + psize, + pssize, + uint8, + uint16, + uint24, + uint32, + uint64, + void_ptr, +) +from .compression import ( + Bz2Compressed, + Compressed, + LZMACompressed, + LZOCompressed, + ZLibCompressed, +) +from .conditional import ( + Branch, + ConditionalChain, + Else, + ElseIf, + End, + If, + Otherwise, + Start, + When, +) from .crypto import And, Encrypted, KeyCipher, Or, Xor -from .digest import (CTX_DIGEST, CTX_DIGEST_ALGO, CTX_DIGEST_HOOK, - CTX_DIGEST_OBJ, HMAC, Adler, Adler_Algo, Adler_Field, - Algorithm, Crc32, Crc32_Algo, Crc32_Field, Digest, - DigestField, DigestFieldAction, HMACAlgorithm, Md5, - Md5_Algo, Md5_Field, Sha1, Sha1_Algo, Sha1_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) +from .digest import ( + CTX_DIGEST, + CTX_DIGEST_ALGO, + CTX_DIGEST_HOOK, + CTX_DIGEST_OBJ, + HMAC, + Adler, + Adler_Algo, + Adler_Field, + Algorithm, + Crc32, + Crc32_Algo, + Crc32_Field, + Digest, + DigestField, + DigestFieldAction, + HMACAlgorithm, + Md5, + Md5_Algo, + Md5_Field, + Sha1, + Sha1_Algo, + Sha1_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, +) 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 .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__ = [ diff --git a/src/caterpillar/fields/_base.py b/src/caterpillar/fields/_base.py old mode 100755 new mode 100644 index 486c9a11..73ef7d7f --- a/src/caterpillar/fields/_base.py +++ b/src/caterpillar/fields/_base.py @@ -20,17 +20,36 @@ from typing_extensions import Self, TypeVar, override from caterpillar import registry -from caterpillar.abc import (_IT, _OT, _ArchLike, _ContainsStruct, - _ContextLambda, _ContextLike, _EndianLike, - _GreedyType, _LengthT, _OptionLike, _PrefixedType, - _StreamType, _StructLike, _SwitchOptionsT) -from caterpillar.byteorder import (O_DEFAULT_ARCH, O_DEFAULT_ENDIAN, - LittleEndian, system_arch) -from caterpillar.context import (CTX_FIELD, CTX_OFFSETS, CTX_SEQ, CTX_STREAM, - CTX_VALUE) -from caterpillar.exception import (DynamicSizeError, InvalidValueError, - OptionError, StructException, - ValidationError) +from caterpillar.abc import ( + _IT, + _OT, + _ArchLike, + _ContainsStruct, + _ContextLambda, + _ContextLike, + _EndianLike, + _GreedyType, + _LengthT, + _OptionLike, + _PrefixedType, + _StreamType, + _StructLike, + _SwitchOptionsT, +) +from caterpillar.byteorder import ( + O_DEFAULT_ARCH, + O_DEFAULT_ENDIAN, + LittleEndian, + system_arch, +) +from caterpillar.context import CTX_FIELD, CTX_OFFSETS, CTX_SEQ, CTX_STREAM, CTX_VALUE +from caterpillar.exception import ( + DynamicSizeError, + InvalidValueError, + OptionError, + StructException, + ValidationError, +) from caterpillar.options import F_DYNAMIC, GLOBAL_FIELD_FLAGS from caterpillar.shared import PackMixin, UnpackMixin, getstruct, typeof diff --git a/src/caterpillar/fields/_mixin.py b/src/caterpillar/fields/_mixin.py old mode 100755 new mode 100644 index b69c26db..66df01be --- a/src/caterpillar/fields/_mixin.py +++ b/src/caterpillar/fields/_mixin.py @@ -21,9 +21,18 @@ from typing_extensions import TypeVar, overload, override from caterpillar._common import WithoutContextVar, pack_seq, unpack_seq -from caterpillar.abc import (_IT, _OT, _ArgType, _ContextLambda, _ContextLike, - _EndianLike, _LengthT, _OptionLike, _StructLike, - _SwitchOptionsT) +from caterpillar.abc import ( + _IT, + _OT, + _ArgType, + _ContextLambda, + _ContextLike, + _EndianLike, + _LengthT, + _OptionLike, + _StructLike, + _SwitchOptionsT, +) from caterpillar.byteorder import byteorder from caterpillar.context import CTX_SEQ, CTX_STREAM from caterpillar.options import Flag diff --git a/src/caterpillar/fields/common.py b/src/caterpillar/fields/common.py old mode 100755 new mode 100644 index 30548195..ec00c0af --- a/src/caterpillar/fields/common.py +++ b/src/caterpillar/fields/common.py @@ -24,19 +24,33 @@ from typing import Any, Callable, Generic from uuid import UUID -from typing_extensions import (Buffer, Final, Self, SupportsFloat, - SupportsIndex, TypeVar, override) +from typing_extensions import ( + Buffer, + Final, + Self, + SupportsFloat, + SupportsIndex, + TypeVar, + override, +) from caterpillar import registry from caterpillar._common import WithoutContextVar, read_exact -from caterpillar.abc import (_IT, _OT, _ContextLambda, _ContextLike, - _EndianLike, _GreedyType, _LengthT, _PrefixedType, - _StreamType, _StructLike) -from caterpillar.byteorder import (LITTLE_ENDIAN_FMT, O_DEFAULT_ENDIAN, - LittleEndian) +from caterpillar.abc import ( + _IT, + _OT, + _ContextLambda, + _ContextLike, + _EndianLike, + _GreedyType, + _LengthT, + _PrefixedType, + _StreamType, + _StructLike, +) +from caterpillar.byteorder import LITTLE_ENDIAN_FMT, O_DEFAULT_ENDIAN, LittleEndian from caterpillar.context import CTX_FIELD, CTX_SEQ, CTX_STREAM -from caterpillar.exception import (DynamicSizeError, InvalidValueError, - ValidationError) +from caterpillar.exception import DynamicSizeError, InvalidValueError, ValidationError from caterpillar.options import GLOBAL_FIELD_FLAGS, Flag from caterpillar.shared import getstruct, typeof diff --git a/src/caterpillar/fields/compression.py b/src/caterpillar/fields/compression.py old mode 100755 new mode 100644 index 7342028d..40fca3b4 --- a/src/caterpillar/fields/compression.py +++ b/src/caterpillar/fields/compression.py @@ -17,8 +17,7 @@ from typing_extensions import override -from caterpillar.abc import (_ContainsStruct, _ContextLike, _LengthT, - _StructLike) +from caterpillar.abc import _ContainsStruct, _ContextLike, _LengthT, _StructLike from caterpillar.fields._mixin import get_kwargs from caterpillar.fields.common import Bytes, Transformer from caterpillar.shared import getstruct, hasstruct diff --git a/src/caterpillar/fields/conditional.py b/src/caterpillar/fields/conditional.py old mode 100755 new mode 100644 index befb89fd..6c3ae00c --- a/src/caterpillar/fields/conditional.py +++ b/src/caterpillar/fields/conditional.py @@ -21,11 +21,16 @@ from typing_extensions import Final, Self, override -from caterpillar.abc import (_AnnotationT, _ArchLike, _ContextLambda, - _ContextLike, _EndianLike, _OptionLike, - _StructLike) -from caterpillar.context import (BinaryExpression, ConditionContext, - UnaryExpression) +from caterpillar.abc import ( + _AnnotationT, + _ArchLike, + _ContextLambda, + _ContextLike, + _EndianLike, + _OptionLike, + _StructLike, +) +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 diff --git a/src/caterpillar/fields/crypto.py b/src/caterpillar/fields/crypto.py old mode 100755 new mode 100644 index 868cebab..3710b48e --- a/src/caterpillar/fields/crypto.py +++ b/src/caterpillar/fields/crypto.py @@ -20,8 +20,14 @@ from typing_extensions import override -from caterpillar.abc import (_IT, _ArgType, _ContextLambda, _ContextLike, - _GreedyType, _StructLike) +from caterpillar.abc import ( + _IT, + _ArgType, + _ContextLambda, + _ContextLike, + _GreedyType, + _StructLike, +) from caterpillar.context import CTX_STREAM from caterpillar.exception import InvalidValueError, UnsupportedOperation @@ -29,8 +35,11 @@ from .common import Bytes, Memory if TYPE_CHECKING: - from cryptography.hazmat.primitives.ciphers import (CipherAlgorithm, - CipherContext, modes) + from cryptography.hazmat.primitives.ciphers import ( + 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 diff --git a/src/caterpillar/fields/hook.py b/src/caterpillar/fields/hook.py old mode 100755 new mode 100644 diff --git a/src/caterpillar/fields/net.py b/src/caterpillar/fields/net.py old mode 100755 new mode 100644 diff --git a/src/caterpillar/fields/pointer.py b/src/caterpillar/fields/pointer.py old mode 100755 new mode 100644 index 532a57c8..8e0eb5e6 --- a/src/caterpillar/fields/pointer.py +++ b/src/caterpillar/fields/pointer.py @@ -18,8 +18,7 @@ from typing_extensions import TypeVar, override from caterpillar._common import WithoutContextVar -from caterpillar.abc import (_IT, _ContextLambda, _ContextLike, _StreamType, - _StructLike) +from caterpillar.abc import _IT, _ContextLambda, _ContextLike, _StreamType, _StructLike from caterpillar.byteorder import Arch from caterpillar.context import CTX_ARCH, CTX_FIELD, CTX_SEQ, CTX_STREAM from caterpillar.exception import DelegationError, StructException @@ -27,8 +26,20 @@ from caterpillar.shared import getstruct from ._mixin import FieldStruct -from .common import (Int, UInt, int8, int16, int24, int32, int64, uint8, - uint16, uint24, uint32, uint64) +from .common import ( + Int, + UInt, + int8, + int16, + int24, + int32, + int64, + uint8, + uint16, + uint24, + uint32, + uint64, +) _PtrValueT = TypeVar("_PtrValueT", default=None) diff --git a/src/caterpillar/fields/varint.py b/src/caterpillar/fields/varint.py old mode 100755 new mode 100644 index 3ac626f2..7ed0dc54 --- a/src/caterpillar/fields/varint.py +++ b/src/caterpillar/fields/varint.py @@ -19,11 +19,9 @@ from typing_extensions import override from caterpillar.abc import _ContextLike, _EndianLike, _StreamType -from caterpillar.byteorder import (LITTLE_ENDIAN_FMT, O_DEFAULT_ENDIAN, - LittleEndian) +from caterpillar.byteorder import LITTLE_ENDIAN_FMT, O_DEFAULT_ENDIAN, LittleEndian from caterpillar.context import CTX_FIELD, CTX_STREAM -from caterpillar.exception import (DynamicSizeError, InvalidValueError, - StreamError) +from caterpillar.exception import DynamicSizeError, InvalidValueError, StreamError from caterpillar.options import Flag from ._mixin import FieldStruct diff --git a/src/caterpillar/model/__init__.py b/src/caterpillar/model/__init__.py index 7b250715..9565e211 100644 --- a/src/caterpillar/model/__init__.py +++ b/src/caterpillar/model/__init__.py @@ -13,15 +13,40 @@ # 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 ._bitfield import (DEFAULT_ALIGNMENT, Bitfield, BitfieldDefMixin, - BitfieldEntry, BitfieldGroup, BitfieldValueFactory, - CharFactory, EndGroup, EnumFactory, NewGroup, - SetAlignment, bitfield, bitfield_factory, getbits, - issigned) -from ._struct import (Invisible, Struct, StructDefMixin, UnionHook, struct, - struct_factory, union) -from ._template import (TemplateFieldRef, TemplateTypeVar, derive, field_of, - istemplate, template) +from ._bitfield import ( + DEFAULT_ALIGNMENT, + Bitfield, + BitfieldDefMixin, + BitfieldEntry, + BitfieldGroup, + BitfieldValueFactory, + CharFactory, + EndGroup, + EnumFactory, + NewGroup, + SetAlignment, + bitfield, + bitfield_factory, + getbits, + issigned, +) +from ._struct import ( + Invisible, + Struct, + StructDefMixin, + UnionHook, + struct, + struct_factory, + union, +) +from ._template import ( + TemplateFieldRef, + TemplateTypeVar, + derive, + field_of, + istemplate, + template, +) from .provider import pack, pack_file, pack_into, sizeof, unpack, unpack_file __all__ = [ diff --git a/src/caterpillar/model/_base.py b/src/caterpillar/model/_base.py old mode 100755 new mode 100644 index 5f3a6122..2a2e2754 --- a/src/caterpillar/model/_base.py +++ b/src/caterpillar/model/_base.py @@ -21,16 +21,34 @@ 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_OBJECT, CTX_PATH, CTX_ROOT, - CTX_SEQ, CTX_STREAM, O_CONTEXT_FACTORY, - Context) +from caterpillar.abc import ( + _ActionLike, + _ArchLike, + _ContextLambda, + _ContextLike, + _EndianLike, + _OptionLike, + _StreamType, + _StructLike, +) +from caterpillar.context import ( + CTX_FIELD, + CTX_OBJECT, + CTX_PATH, + CTX_ROOT, + CTX_SEQ, + CTX_STREAM, + O_CONTEXT_FACTORY, + Context, +) 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_REPLACE_TYPES, S_UNION) +from caterpillar.options import ( + S_DISCARD_CONST, + S_DISCARD_UNNAMED, + S_REPLACE_TYPES, + S_UNION, +) from caterpillar.shared import ATTR_ACTION_PACK, ATTR_ACTION_UNPACK, Action diff --git a/src/caterpillar/model/_bitfield.py b/src/caterpillar/model/_bitfield.py old mode 100755 new mode 100644 index a5139b82..7bfccb6a --- a/src/caterpillar/model/_bitfield.py +++ b/src/caterpillar/model/_bitfield.py @@ -18,24 +18,53 @@ from collections.abc import Iterable from typing import Any, Callable, Final, Generic, Literal -from typing_extensions import (ClassVar, Self, TypeVar, dataclass_transform, - overload, override) - -from caterpillar.abc import (_ActionLike, _ArchLike, _ContextLike, _EndianLike, - _OptionLike, _StructLike) -from caterpillar.byteorder import (LITTLE_ENDIAN_FMT, O_DEFAULT_ENDIAN, - LittleEndian) -from caterpillar.context import (CTX_FIELD, CTX_OBJECT, CTX_PATH, CTX_STREAM, - O_CONTEXT_FACTORY, Context) +from typing_extensions import ( + ClassVar, + Self, + TypeVar, + dataclass_transform, + overload, + override, +) + +from caterpillar.abc import ( + _ActionLike, + _ArchLike, + _ContextLike, + _EndianLike, + _OptionLike, + _StructLike, +) +from caterpillar.byteorder import LITTLE_ENDIAN_FMT, O_DEFAULT_ENDIAN, LittleEndian +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, Flag) -from caterpillar.shared import (ATTR_ACTION_PACK, ATTR_ACTION_UNPACK, - ATTR_BITS, ATTR_SIGNED, typeof) +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, + Flag, +) +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 diff --git a/src/caterpillar/model/_struct.py b/src/caterpillar/model/_struct.py old mode 100755 new mode 100644 index 888bf19d..336aa93f --- a/src/caterpillar/model/_struct.py +++ b/src/caterpillar/model/_struct.py @@ -20,18 +20,36 @@ from types import TracebackType from typing import Any, Callable, Generic, Literal, ParamSpec, TypeVar -from typing_extensions import (Buffer, ClassVar, Self, dataclass_transform, - overload, override) +from typing_extensions import ( + Buffer, + ClassVar, + Self, + dataclass_transform, + overload, + override, +) from caterpillar import registry -from caterpillar.abc import (_ArchLike, _ContextLike, _EndianLike, _LengthT, - _OptionLike, _StreamType, _StructLike) +from caterpillar.abc import ( + _ArchLike, + _ContextLike, + _EndianLike, + _LengthT, + _OptionLike, + _StreamType, + _StructLike, +) 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.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 diff --git a/src/caterpillar/model/_template.py b/src/caterpillar/model/_template.py old mode 100755 new mode 100644 index 23f79efe..8dd3e32e --- a/src/caterpillar/model/_template.py +++ b/src/caterpillar/model/_template.py @@ -20,13 +20,19 @@ import types from hashlib import md5 from types import ModuleType -from typing import (Annotated, Any, Callable, Generic, TypeVar, get_args, - get_origin) +from typing import Annotated, Any, Callable, Generic, TypeVar, get_args, get_origin from typing_extensions import dataclass_transform, overload, override -from caterpillar.abc import (_ArchLike, _ContextLambda, _EndianLike, - _GreedyType, _LengthT, _StructLike, _SwitchLambda) +from caterpillar.abc import ( + _ArchLike, + _ContextLambda, + _EndianLike, + _GreedyType, + _LengthT, + _StructLike, + _SwitchLambda, +) from caterpillar.fields import INVALID_DEFAULT, Field from caterpillar.model import Invisible, Struct from caterpillar.options import S_UNION diff --git a/src/caterpillar/model/provider.py b/src/caterpillar/model/provider.py index fc6c5e04..e72357c6 100644 --- a/src/caterpillar/model/provider.py +++ b/src/caterpillar/model/provider.py @@ -21,15 +21,26 @@ 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.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.context import CTX_STREAM, O_CONTEXT_FACTORY, Context from caterpillar.exception import DynamicSizeError -from caterpillar.shared import (ATTR_PACK, MODE_PACK, MODE_UNPACK, getstruct, - hasstruct) +from caterpillar.shared import ATTR_PACK, MODE_PACK, MODE_UNPACK, getstruct, hasstruct @overload diff --git a/src/caterpillar/options.py b/src/caterpillar/options.py old mode 100755 new mode 100644 diff --git a/src/caterpillar/py.py b/src/caterpillar/py.py index a1200c71..c657609e 100644 --- a/src/caterpillar/py.py +++ b/src/caterpillar/py.py @@ -15,38 +15,126 @@ from typing import Annotated as f from ._common import WithoutContextVar, iseof, pack_seq, unpack_seq -from .byteorder import (AARCH64, AMD, AMD64, ARM, ARM64, MIPS, MIPS64, RISC_V, - RISC_V64, SPARC, SPARC64, Arch, BigEndian, ByteOrder, - Dynamic, DynByteOrder, LittleEndian, Native, NetEndian, - PowerPC, PowerPC64, SysNative, system_arch, x86, - x86_64) -from .context import (CTX_ARCH, CTX_FIELD, CTX_INDEX, CTX_OBJECT, CTX_OFFSETS, - CTX_ORDER, CTX_PARENT, CTX_PATH, CTX_POS, CTX_ROOT, - CTX_SEQ, CTX_STREAM, CTX_VALUE, O_CONTEXT_FACTORY, - BinaryExpression, ConditionContext, Context, - ContextLength, ContextPath, ExprMixin, SetContextVar, - UnaryExpression, ctx, parent, parentctx, root, this) -from .exception import (DelegationError, DynamicSizeError, InvalidValueError, - OptionError, Stop, StreamError, StructException, - ValidationError) +from .byteorder import ( + AARCH64, + AMD, + AMD64, + ARM, + ARM64, + MIPS, + MIPS64, + RISC_V, + RISC_V64, + SPARC, + SPARC64, + Arch, + BigEndian, + ByteOrder, + Dynamic, + DynByteOrder, + LittleEndian, + Native, + NetEndian, + PowerPC, + PowerPC64, + SysNative, + system_arch, + x86, + x86_64, +) +from .context import ( + CTX_ARCH, + CTX_FIELD, + CTX_INDEX, + CTX_OBJECT, + CTX_OFFSETS, + CTX_ORDER, + CTX_PARENT, + CTX_PATH, + CTX_POS, + CTX_ROOT, + CTX_SEQ, + CTX_STREAM, + CTX_VALUE, + O_CONTEXT_FACTORY, + BinaryExpression, + ConditionContext, + Context, + ContextLength, + ContextPath, + ExprMixin, + SetContextVar, + UnaryExpression, + ctx, + parent, + parentctx, + root, + this, +) +from .exception import ( + DelegationError, + DynamicSizeError, + InvalidValueError, + OptionError, + Stop, + StreamError, + StructException, + ValidationError, +) from .fields import * # noqa from .model import * # noqa -from .options import (B_GROUP_END, B_GROUP_KEEP, B_GROUP_NEW, B_NO_AUTO_BOOL, - B_OVERWRITE_ALIGNMENT, F_DYNAMIC, F_KEEP_POSITION, - F_OFFSET_OVERRIDE, F_SEQUENTIAL, GLOBAL_BITFIELD_FLAGS, - GLOBAL_FIELD_FLAGS, GLOBAL_STRUCT_OPTIONS, - GLOBAL_UNION_OPTIONS, O_ARRAY_FACTORY, S_ADD_BYTES, - S_DISCARD_CONST, S_DISCARD_UNNAMED, S_EVAL_ANNOTATIONS, - S_REPLACE_TYPES, S_SLOTS, S_UNION, Flag, get_flag, - get_flags, has_flag, set_field_flags, set_struct_flags, - set_union_flags) +from .options import ( + B_GROUP_END, + B_GROUP_KEEP, + B_GROUP_NEW, + B_NO_AUTO_BOOL, + B_OVERWRITE_ALIGNMENT, + F_DYNAMIC, + F_KEEP_POSITION, + F_OFFSET_OVERRIDE, + F_SEQUENTIAL, + GLOBAL_BITFIELD_FLAGS, + GLOBAL_FIELD_FLAGS, + GLOBAL_STRUCT_OPTIONS, + GLOBAL_UNION_OPTIONS, + O_ARRAY_FACTORY, + S_ADD_BYTES, + S_DISCARD_CONST, + S_DISCARD_UNNAMED, + S_EVAL_ANNOTATIONS, + S_REPLACE_TYPES, + S_SLOTS, + S_UNION, + Flag, + get_flag, + get_flags, + has_flag, + set_field_flags, + set_struct_flags, + set_union_flags, +) + # use this module to import everything Python related from .registry import TypeConverter, annotation_registry, to_struct -from .shared import (ATTR_ACTION_PACK, ATTR_ACTION_UNPACK, ATTR_BITS, - ATTR_BYTEORDER, ATTR_PACK, ATTR_SIGNED, ATTR_STRUCT, - ATTR_TEMPLATE, ATTR_TYPE, ATTR_UNPACK, MODE_PACK, - MODE_UNPACK, Action, constval, getstruct, hasstruct, - typeof) +from .shared import ( + ATTR_ACTION_PACK, + ATTR_ACTION_UNPACK, + ATTR_BITS, + ATTR_BYTEORDER, + ATTR_PACK, + ATTR_SIGNED, + ATTR_STRUCT, + ATTR_TEMPLATE, + ATTR_TYPE, + ATTR_UNPACK, + MODE_PACK, + MODE_UNPACK, + Action, + constval, + getstruct, + hasstruct, + typeof, +) __all__ = [ "AARCH64", diff --git a/src/caterpillar/shared.py b/src/caterpillar/shared.py index 7bd8a07c..de55cda9 100644 --- a/src/caterpillar/shared.py +++ b/src/caterpillar/shared.py @@ -46,9 +46,18 @@ from typing_extensions import Buffer, Final, Literal, TypeIs, override -from caterpillar.abc import (_IT, _OT, _ActionLike, _ArchLike, _ContainsStruct, - _ContextLambda, _EndianLike, _StreamType, - _StructLike, _SupportsType) +from caterpillar.abc import ( + _IT, + _OT, + _ActionLike, + _ArchLike, + _ContainsStruct, + _ContextLambda, + _EndianLike, + _StreamType, + _StructLike, + _SupportsType, +) if TYPE_CHECKING: from caterpillar.fields import FieldStruct diff --git a/src/caterpillar/shortcuts.py b/src/caterpillar/shortcuts.py index 1783130f..6e016237 100644 --- a/src/caterpillar/shortcuts.py +++ b/src/caterpillar/shortcuts.py @@ -15,9 +15,22 @@ from typing import Annotated as f from . import options as opt -from .byteorder import (AARCH64, AMD, AMD64, ARM, ARM64, RISC_V, RISC_V64, - BigEndian, Dynamic, LittleEndian, PowerPC, PowerPC64, - x86, x86_64) +from .byteorder import ( + AARCH64, + AMD, + AMD64, + ARM, + ARM64, + RISC_V, + RISC_V64, + BigEndian, + Dynamic, + LittleEndian, + PowerPC, + PowerPC64, + x86, + x86_64, +) from .context import ContextLength as lenof from .context import ContextPath from .context import ctx @@ -28,8 +41,17 @@ from .context import this 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) +from .model import ( + bitfield, + pack, + pack_file, + pack_into, + sizeof, + struct, + union, + unpack, + unpack_file, +) from .registry import to_struct from .shared import getstruct, hasstruct, typeof diff --git a/src/caterpillar/types.py b/src/caterpillar/types.py index f1ca8069..3bb24172 100644 --- a/src/caterpillar/types.py +++ b/src/caterpillar/types.py @@ -16,10 +16,31 @@ import uuid from datetime import datetime -from .fields.common import (CString, Pass, Timestamp, Uuid, boolean, char, - double, float16, float32, float64, int8, int16, - int24, int32, int64, psize, pssize, uint8, uint16, - uint24, uint32, uint64, void_ptr) +from .fields.common import ( + CString, + Pass, + Timestamp, + Uuid, + boolean, + char, + double, + float16, + float32, + float64, + int8, + int16, + int24, + int32, + int64, + psize, + pssize, + uint8, + uint16, + uint24, + uint32, + uint64, + void_ptr, +) from .fields.net import MAC, IPv4Address, IPv6Address from .fields.varint import vint from .shortcuts import f diff --git a/src/code_gen/genapi.py b/src/code_gen/genapi.py index 1db8c995..d6ed6170 100644 --- a/src/code_gen/genapi.py +++ b/src/code_gen/genapi.py @@ -7,8 +7,7 @@ import dataclasses import pathlib -from caterpillar_api import (CP_FUNC_API, CP_SRC, CP_TYPE_API, CP_TYPES, - cp_api_functions) +from caterpillar_api import CP_FUNC_API, CP_SRC, CP_TYPE_API, CP_TYPES, cp_api_functions from typing_extensions import override diff --git a/test/_C/test_protocols.py b/test/_C/test_protocols.py index b8e5b9a3..302baf60 100644 --- a/test/_C/test_protocols.py +++ b/test/_C/test_protocols.py @@ -4,11 +4,25 @@ import caterpillar if caterpillar.native_support(): - from caterpillar.abc import (_ArchLike, _ContextLike, _EndianLike, - _OptionLike, _StructLike) - from caterpillar.c import (AtOffset, Atom, BuiltinAtom, Conditional, - Repeated, Switch, c_Arch, c_Context, c_Endian, - c_Option) + from caterpillar.abc import ( + _ArchLike, + _ContextLike, + _EndianLike, + _OptionLike, + _StructLike, + ) + from caterpillar.c import ( + AtOffset, + Atom, + BuiltinAtom, + Conditional, + Repeated, + Switch, + c_Arch, + c_Context, + c_Endian, + c_Option, + ) # === PROTOCOL COMPLIANCE === # All tests below should verify the protocol compliance of all classes diff --git a/test/_Py/fields/test_py_aligned.py b/test/_Py/fields/test_py_aligned.py index 61a09a4a..6afe6639 100644 --- a/test/_Py/fields/test_py_aligned.py +++ b/test/_Py/fields/test_py_aligned.py @@ -2,8 +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_bytes.py b/test/_Py/fields/test_py_bytes.py index 479f9409..66b92380 100644 --- a/test/_Py/fields/test_py_bytes.py +++ b/test/_Py/fields/test_py_bytes.py @@ -2,8 +2,7 @@ import pytest -from caterpillar.py import (Bytes, ValidationError, f, pack, struct, this, - unpack) +from caterpillar.py import Bytes, ValidationError, f, pack, struct, this, 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 706c4084..bf69f250 100644 --- a/test/_Py/fields/test_py_chain.py +++ b/test/_Py/fields/test_py_chain.py @@ -1,5 +1,4 @@ -from caterpillar.py import (CTX_STREAM, Bytes, 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 3ae9fbc3..e5f4f9a1 100644 --- a/test/_Py/fields/test_py_compressed.py +++ b/test/_Py/fields/test_py_compressed.py @@ -4,8 +4,12 @@ import pytest -from caterpillar.fields.compression import (Bz2Compressed, Compressed, - LZMACompressed, ZLibCompressed) +from caterpillar.fields.compression import ( + Bz2Compressed, + Compressed, + LZMACompressed, + ZLibCompressed, +) from caterpillar.py import Bytes, pack, root, unpack diff --git a/test/_Py/fields/test_py_computed.py b/test/_Py/fields/test_py_computed.py index 7b45dab8..de0dba82 100644 --- a/test/_Py/fields/test_py_computed.py +++ b/test/_Py/fields/test_py_computed.py @@ -1,7 +1,6 @@ import io -from caterpillar.py import (Bytes, Computed, Context, f, pack, struct, this, - unpack) +from caterpillar.py import Bytes, Computed, Context, f, pack, struct, this, unpack from caterpillar.types import uint8_t diff --git a/test/_Py/fields/test_py_const.py b/test/_Py/fields/test_py_const.py index 6bbb58c0..58c7819a 100644 --- a/test/_Py/fields/test_py_const.py +++ b/test/_Py/fields/test_py_const.py @@ -1,8 +1,7 @@ import pytest from caterpillar.exception import ValidationError -from caterpillar.py import (Const, ConstBytes, Invisible, f, pack, struct, - uint8, unpack) +from caterpillar.py import Const, ConstBytes, Invisible, f, pack, struct, uint8, unpack def test_const_pack(): diff --git a/test/_Py/fields/test_py_digest.py b/test/_Py/fields/test_py_digest.py index ff836fad..b4397bab 100644 --- a/test/_Py/fields/test_py_digest.py +++ b/test/_Py/fields/test_py_digest.py @@ -4,8 +4,15 @@ import pytest from caterpillar.exception import ValidationError -from caterpillar.fields.digest import (DigestField, Md5, Md5_Algo, Md5_Field, - Sha2_256, Sha2_256_Algo, Sha2_256_Field) +from caterpillar.fields.digest import ( + DigestField, + Md5, + Md5_Algo, + Md5_Field, + Sha2_256, + 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_enum.py b/test/_Py/fields/test_py_enum.py index 6ebcf4b7..c0778b7f 100644 --- a/test/_Py/fields/test_py_enum.py +++ b/test/_Py/fields/test_py_enum.py @@ -2,8 +2,7 @@ import pytest -from caterpillar.py import (Enum, InvalidValueError, ValidationError, pack, - uint8, unpack) +from caterpillar.py import Enum, InvalidValueError, ValidationError, pack, uint8, unpack class Color(enum.Enum): diff --git a/test/_Py/fields/test_py_if.py b/test/_Py/fields/test_py_if.py index fdf3108b..27fac3c6 100644 --- a/test/_Py/fields/test_py_if.py +++ b/test/_Py/fields/test_py_if.py @@ -3,10 +3,27 @@ import pytest -from caterpillar.py import (BigEndian, Branch, Else, ElseIf, End, If, - Invisible, LittleEndian, Otherwise, Start, - StructException, When, f, pack, struct, this, - uint8, uint16, unpack) +from caterpillar.py import ( + BigEndian, + Branch, + Else, + ElseIf, + End, + If, + Invisible, + LittleEndian, + Otherwise, + Start, + StructException, + When, + f, + pack, + struct, + this, + uint8, + uint16, + unpack, +) # <3.14 SYNTAX: diff --git a/test/_Py/fields/test_py_int.py b/test/_Py/fields/test_py_int.py index 071fa762..d6d24787 100644 --- a/test/_Py/fields/test_py_int.py +++ b/test/_Py/fields/test_py_int.py @@ -1,7 +1,6 @@ import pytest -from caterpillar.py import (BigEndian, Int, LittleEndian, ValidationError, - pack, unpack) +from caterpillar.py import BigEndian, Int, LittleEndian, ValidationError, pack, unpack # --------------------------------------------------------------------------- # diff --git a/test/_Py/fields/test_py_lengthfor.py b/test/_Py/fields/test_py_lengthfor.py index ce6e8c10..1fc57ad2 100644 --- a/test/_Py/fields/test_py_lengthfor.py +++ b/test/_Py/fields/test_py_lengthfor.py @@ -2,8 +2,19 @@ import pytest -from caterpillar.py import (AsLengthRef, BigEndian, Bytes, String, f, pack, - struct, this, uint8, uint16, unpack) +from caterpillar.py import ( + AsLengthRef, + BigEndian, + Bytes, + String, + f, + pack, + struct, + this, + uint8, + uint16, + unpack, +) # --------------------------------------------------------------------------- # diff --git a/test/_Py/fields/test_py_memory.py b/test/_Py/fields/test_py_memory.py index 0ac25b11..8dbcb841 100644 --- a/test/_Py/fields/test_py_memory.py +++ b/test/_Py/fields/test_py_memory.py @@ -1,7 +1,14 @@ import pytest -from caterpillar.py import (DynamicSizeError, Memory, ValidationError, pack, - root, sizeof, unpack) +from caterpillar.py import ( + DynamicSizeError, + Memory, + ValidationError, + pack, + root, + sizeof, + unpack, +) def test_py_memory_unpack(): diff --git a/test/_Py/fields/test_py_padding.py b/test/_Py/fields/test_py_padding.py index 1c08b5cd..e8b42af4 100644 --- a/test/_Py/fields/test_py_padding.py +++ b/test/_Py/fields/test_py_padding.py @@ -2,9 +2,24 @@ import pytest -from caterpillar.py import (DynamicSizeError, Padded, Padding, PostPad, PrePad, - ValidationError, constval, f, pack, padding, - sizeof, struct, this, uint8, uint16, unpack) +from caterpillar.py import ( + DynamicSizeError, + Padded, + Padding, + PostPad, + PrePad, + ValidationError, + constval, + f, + pack, + padding, + sizeof, + struct, + this, + uint8, + uint16, + unpack, +) from caterpillar.types import uint8_t diff --git a/test/_Py/fields/test_py_pointer.py b/test/_Py/fields/test_py_pointer.py index 8cbdbbd7..5eee4385 100644 --- a/test/_Py/fields/test_py_pointer.py +++ b/test/_Py/fields/test_py_pointer.py @@ -2,8 +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_prefixed.py b/test/_Py/fields/test_py_prefixed.py index a5a1212b..77ce7d2b 100644 --- a/test/_Py/fields/test_py_prefixed.py +++ b/test/_Py/fields/test_py_prefixed.py @@ -2,8 +2,16 @@ import pytest -from caterpillar.py import (BigEndian, Bytes, Prefixed, ValidationError, pack, - uint8, uint16, unpack) +from caterpillar.py import ( + BigEndian, + Bytes, + Prefixed, + ValidationError, + pack, + uint8, + uint16, + unpack, +) def test_prefixed_pack_unpack(): diff --git a/test/_Py/fields/test_py_pystruct.py b/test/_Py/fields/test_py_pystruct.py index e019cc2c..0db40af8 100644 --- a/test/_Py/fields/test_py_pystruct.py +++ b/test/_Py/fields/test_py_pystruct.py @@ -2,10 +2,30 @@ import pytest -from caterpillar.py import (BigEndian, Context, LittleEndian, SysNative, - boolean, char, float16, float32, float64, int8, - int16, int32, int64, pack, psize, pssize, uint8, - uint16, uint32, uint64, unpack, void_ptr) +from caterpillar.py import ( + BigEndian, + Context, + LittleEndian, + SysNative, + boolean, + char, + float16, + float32, + float64, + int8, + int16, + int32, + int64, + pack, + psize, + pssize, + uint8, + uint16, + uint32, + uint64, + unpack, + void_ptr, +) def test_integer_formats_sizes_and_endianness(): diff --git a/test/_Py/model/test_bitfield.py b/test/_Py/model/test_bitfield.py index 0d3925ae..75a0811e 100644 --- a/test/_Py/model/test_bitfield.py +++ b/test/_Py/model/test_bitfield.py @@ -3,14 +3,30 @@ import pytest from caterpillar.fields import Bytes, uint8, uint16 -from caterpillar.model import (Bitfield, CharFactory, EndGroup, Invisible, - SetAlignment, bitfield, pack, sizeof, unpack) +from caterpillar.model import ( + Bitfield, + CharFactory, + EndGroup, + Invisible, + SetAlignment, + bitfield, + pack, + sizeof, + unpack, +) 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 -from caterpillar.types import (balign_t, int1_t, int2_t, int3_t, uint16_t, - uint24_t, uint32_t) +from caterpillar.types import ( + balign_t, + int1_t, + int2_t, + int3_t, + uint16_t, + uint24_t, + uint32_t, +) def test_bitfield_syntax__standard(): diff --git a/test/_Py/model/test_template.py b/test/_Py/model/test_template.py index 9f3b4deb..4fe87528 100644 --- a/test/_Py/model/test_template.py +++ b/test/_Py/model/test_template.py @@ -1,7 +1,17 @@ from typing_extensions import Generic, TypeVar -from caterpillar.py import (BigEndian, derive, f, field_of, pack, struct, - template, uint8, uint16, unpack) +from caterpillar.py import ( + BigEndian, + derive, + f, + field_of, + pack, + struct, + template, + uint8, + uint16, + unpack, +) from caterpillar.types import uint8_t diff --git a/test/_Py/test_arch.py b/test/_Py/test_arch.py index b6581f81..57e04d25 100644 --- a/test/_Py/test_arch.py +++ b/test/_Py/test_arch.py @@ -1,7 +1,20 @@ import sys -from caterpillar.py import (Arch, Field, f, intptr, pack, sizeof, struct, - system_arch, uint8, uintptr, unpack, x86, x86_64) +from caterpillar.py import ( + Arch, + Field, + f, + intptr, + pack, + sizeof, + struct, + system_arch, + uint8, + uintptr, + unpack, + x86, + x86_64, +) # --------------------------------------------------------------------------- # diff --git a/test/_Py/test_byteorder.py b/test/_Py/test_byteorder.py index 644c8cbd..32e251ed 100644 --- a/test/_Py/test_byteorder.py +++ b/test/_Py/test_byteorder.py @@ -1,8 +1,22 @@ from caterpillar.abc import _ContextLike # pyright: ignore[reportPrivateUsage] from caterpillar.context import SetContextVar -from caterpillar.py import (CTX_ORDER, BigEndian, Dynamic, DynByteOrder, - Invisible, LittleEndian, ctx, f, pack, struct, - this, uint16, uint32, uint64, unpack) +from caterpillar.py import ( + CTX_ORDER, + BigEndian, + Dynamic, + DynByteOrder, + Invisible, + LittleEndian, + ctx, + f, + pack, + struct, + this, + uint16, + uint32, + uint64, + unpack, +) from caterpillar.types import uint8_t, uint16_t, uint32_t, uint64_t diff --git a/test/_Py/test_flag.py b/test/_Py/test_flag.py index 96b33cc6..26900795 100644 --- a/test/_Py/test_flag.py +++ b/test/_Py/test_flag.py @@ -1,5 +1,10 @@ -from caterpillar.options import (GLOBAL_STRUCT_OPTIONS, Flag, configure, - get_flag, get_flags) +from caterpillar.options import ( + GLOBAL_STRUCT_OPTIONS, + Flag, + configure, + get_flag, + get_flags, +) from caterpillar.py import Context diff --git a/test/_Py/test_protocols.py b/test/_Py/test_protocols.py index 870b057c..49973c80 100644 --- a/test/_Py/test_protocols.py +++ b/test/_Py/test_protocols.py @@ -1,19 +1,61 @@ # 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, KeyCipher, - Lazy, MACAddress, Memory, Pass, Pointer, Prefixed, - PyStructFormattedField, RelativePointer, Sequence, - String, Struct, SysNative, Transformer, UInt, - UnaryExpression, Uuid, VarInt, system_arch) +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, + KeyCipher, + Lazy, + MACAddress, + Memory, + Pass, + Pointer, + Prefixed, + PyStructFormattedField, + RelativePointer, + Sequence, + String, + Struct, + SysNative, + Transformer, + UInt, + UnaryExpression, + Uuid, + VarInt, + system_arch, +) # === PROTOCOL COMPLIANCE === From 3225206e077c5e447d060698c0c53e0a7d98b25c Mon Sep 17 00:00:00 2001 From: MatrixEditor <58256046+MatrixEditor@users.noreply.github.com> Date: Sat, 1 Aug 2026 09:53:10 +0200 Subject: [PATCH 5/8] feat(byteorder): Inherit byteorder plus tests --- pyproject.toml | 19 +- src/caterpillar/_common.py | 2 +- src/caterpillar/context.py | 102 +++++----- src/caterpillar/fields/_base.py | 6 + src/caterpillar/fields/common.py | 50 +++-- src/caterpillar/fields/pointer.py | 62 +++--- src/caterpillar/model/_base.py | 30 ++- src/caterpillar/model/_bitfield.py | 12 +- src/caterpillar/model/_struct.py | 22 +- test/_Py/test_byteorder.py | 314 ++++++++++++++++++++++++++++- 10 files changed, 513 insertions(+), 106 deletions(-) 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/_common.py b/src/caterpillar/_common.py index d9cfe20f..9ae22fdb 100644 --- a/src/caterpillar/_common.py +++ b/src/caterpillar/_common.py @@ -55,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/context.py b/src/caterpillar/context.py index d21e08fc..ef9d9cdc 100644 --- a/src/caterpillar/context.py +++ b/src/caterpillar/context.py @@ -278,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): @@ -361,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) @@ -414,10 +414,10 @@ class Format: """ __slots__: tuple[str, ...] = ( - "func", "annotations", - "namelist", "depth", + "func", + "namelist", ) def __init__(self, condition: _ContextLambda[bool] | bool, depth: int = 2): @@ -649,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. @@ -724,7 +728,7 @@ def __str__(self) -> str: return self.path or "" @property - def parent(self) -> ContextPath[_ContextLike]: + def parent(self) -> "ContextPath[_ContextLike]": """ .. versionchanged:: 2.9.2 Always resolves to the parent context object instead the parent context. @@ -739,7 +743,7 @@ def parent(self) -> ContextPath[_ContextLike]: 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/_base.py b/src/caterpillar/fields/_base.py index 73ef7d7f..ea850522 100644 --- a/src/caterpillar/fields/_base.py +++ b/src/caterpillar/fields/_base.py @@ -573,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 @@ -634,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/common.py b/src/caterpillar/fields/common.py index ec00c0af..80fa7d09 100644 --- a/src/caterpillar/fields/common.py +++ b/src/caterpillar/fields/common.py @@ -16,17 +16,16 @@ import datetime import struct as PyStruct import warnings -from collections.abc import Collection +from collections.abc import Callable, Collection from enum import Enum as _EnumType from functools import cached_property from io import BytesIO from types import NoneType -from typing import Any, Callable, Generic +from typing import Any, Final, Generic from uuid import UUID from typing_extensions import ( Buffer, - Final, Self, SupportsFloat, SupportsIndex, @@ -1680,11 +1679,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] @@ -1693,7 +1697,14 @@ 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 @@ -1708,7 +1719,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( @@ -1719,11 +1739,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", diff --git a/src/caterpillar/fields/pointer.py b/src/caterpillar/fields/pointer.py index 8e0eb5e6..8de5e068 100644 --- a/src/caterpillar/fields/pointer.py +++ b/src/caterpillar/fields/pointer.py @@ -18,7 +18,7 @@ from typing_extensions import TypeVar, override from caterpillar._common import WithoutContextVar -from caterpillar.abc import _IT, _ContextLambda, _ContextLike, _StreamType, _StructLike +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 @@ -47,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. @@ -70,7 +70,7 @@ def get(self): _PtrT = TypeVar("_PtrT", default=pointer) -class Pointer(Generic[_PtrT, _PtrValueT], FieldStruct[int, _PtrT]): +class Pointer(FieldStruct[int, _PtrT], Generic[_PtrT, _PtrValueT]): """ A struct that represents a pointer to another struct within the stream. @@ -82,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. @@ -111,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. @@ -125,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. @@ -139,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) @@ -163,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. @@ -184,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 @@ -194,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 @@ -210,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. @@ -222,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. @@ -237,7 +241,7 @@ def _create( value: int, start: int, model_obj: _PtrValueT | None, - context: _ContextLike, + context: ContextLike, ) -> pointer[_PtrValueT]: """ Create a new pointer object. @@ -253,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 = ( @@ -277,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 = ( @@ -338,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. @@ -356,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/model/_base.py b/src/caterpillar/model/_base.py index 2a2e2754..21b280f6 100644 --- a/src/caterpillar/model/_base.py +++ b/src/caterpillar/model/_base.py @@ -13,6 +13,7 @@ # 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 @@ -39,7 +40,7 @@ CTX_SEQ, CTX_STREAM, O_CONTEXT_FACTORY, - Context, + Context, CTX_PARENT, CTX_ORDER, ) from caterpillar.exception import StructException, ValidationError from caterpillar.fields import INVALID_DEFAULT, Const, Field, FieldMixin @@ -499,9 +500,33 @@ 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: + if self.order is not Inherit: + return 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: + _ = self._resolve_order(context) + factory = O_CONTEXT_FACTORY.value or Context fields = self.fields ctx_path = CTX_PATH @@ -578,6 +603,9 @@ 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: + _ = 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 index 7bfccb6a..d44d66ed 100644 --- a/src/caterpillar/model/_bitfield.py +++ b/src/caterpillar/model/_bitfield.py @@ -35,7 +35,7 @@ _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, @@ -1014,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) @@ -1072,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 index 336aa93f..3a8719ab 100644 --- a/src/caterpillar/model/_struct.py +++ b/src/caterpillar/model/_struct.py @@ -37,7 +37,7 @@ _LengthT, _OptionLike, _StreamType, - _StructLike, + _StructLike, EndianLike, ) from caterpillar.exception import InvalidValueError from caterpillar.fields import INVALID_DEFAULT, Field @@ -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. @@ -348,6 +364,10 @@ def __class_getitem__( """ 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], diff --git a/test/_Py/test_byteorder.py b/test/_Py/test_byteorder.py index 32e251ed..17829cbf 100644 --- a/test/_Py/test_byteorder.py +++ b/test/_Py/test_byteorder.py @@ -1,5 +1,7 @@ -from caterpillar.abc import _ContextLike # pyright: ignore[reportPrivateUsage] +from caterpillar.abc import ContextLike +from caterpillar.byteorder import Inherit from caterpillar.context import SetContextVar +from caterpillar.fields import Field from caterpillar.py import ( CTX_ORDER, BigEndian, @@ -7,15 +9,21 @@ DynByteOrder, Invisible, LittleEndian, + Pointer, + Prefixed, + bitfield, ctx, f, pack, + parent, struct, this, + uint8, uint16, uint32, uint64, unpack, + StructDefMixin, ) from caterpillar.types import uint8_t, uint16_t, uint32_t, uint64_t @@ -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 @@ -201,3 +209,305 @@ class Format: obj = Format(a=0x1234, b=0x56789ABC) data = pack(obj) 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 From c8a4fad5a8016f426ce1d99dbab61dcf4d01daf8 Mon Sep 17 00:00:00 2001 From: MatrixEditor <58256046+MatrixEditor@users.noreply.github.com> Date: Sat, 1 Aug 2026 09:55:16 +0200 Subject: [PATCH 6/8] refactor: add inherit byteorder to exports --- src/caterpillar/py.py | 304 ++++++++++++++++++----------------- src/caterpillar/shortcuts.py | 51 +++--- 2 files changed, 178 insertions(+), 177 deletions(-) diff --git a/src/caterpillar/py.py b/src/caterpillar/py.py index c657609e..a3905648 100644 --- a/src/caterpillar/py.py +++ b/src/caterpillar/py.py @@ -32,6 +32,7 @@ ByteOrder, Dynamic, DynByteOrder, + Inherit, LittleEndian, Native, NetEndian, @@ -138,21 +139,10 @@ __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", @@ -168,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/shortcuts.py b/src/caterpillar/shortcuts.py index 6e016237..3fe3974f 100644 --- a/src/caterpillar/shortcuts.py +++ b/src/caterpillar/shortcuts.py @@ -25,6 +25,7 @@ RISC_V64, BigEndian, Dynamic, + Inherit, LittleEndian, PowerPC, PowerPC64, @@ -32,13 +33,10 @@ x86_64, ) from .context import ContextLength as lenof -from .context import ContextPath -from .context import ctx +from .context import ContextPath, ctx, parent, this from .context import ctx as C -from .context import parent from .context import parent as P from .context import root as G -from .context import this from .fields import Field as F from .model import Sequence as Seq from .model import ( @@ -61,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", ] From e497e437145c9bd8155d9bd36981f64db5e7cceb Mon Sep 17 00:00:00 2001 From: MatrixEditor <58256046+MatrixEditor@users.noreply.github.com> Date: Sat, 8 Aug 2026 16:14:34 +0200 Subject: [PATCH 7/8] feat(byteorder): add default struct order for pack and unpack - normalized byteorder resolution --- src/caterpillar/fields/common.py | 286 +++++++++++++++--------------- src/caterpillar/fields/varint.py | 22 +-- src/caterpillar/model/_base.py | 52 ++++-- src/caterpillar/model/provider.py | 153 ++++++++++------ src/caterpillar/options.py | 3 +- 5 files changed, 283 insertions(+), 233 deletions(-) diff --git a/src/caterpillar/fields/common.py b/src/caterpillar/fields/common.py index 80fa7d09..c328d2a0 100644 --- a/src/caterpillar/fields/common.py +++ b/src/caterpillar/fields/common.py @@ -38,20 +38,20 @@ from caterpillar.abc import ( _IT, _OT, - _ContextLambda, - _ContextLike, - _EndianLike, + ContextLambda, + ContextLike, _GreedyType, _LengthT, _PrefixedType, _StreamType, _StructLike, + EndianLike, ) from caterpillar.byteorder import LITTLE_ENDIAN_FMT, O_DEFAULT_ENDIAN, LittleEndian -from caterpillar.context import CTX_FIELD, CTX_SEQ, CTX_STREAM +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 getstruct, typeof +from caterpillar.shared import ATTR_BYTEORDER, getstruct, typeof from ._base import INVALID_DEFAULT, Field, singleton from ._mixin import ByteOrderMixin, FieldStruct @@ -64,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.). @@ -90,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.", @@ -135,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. @@ -148,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. @@ -158,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. @@ -183,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) @@ -202,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) @@ -233,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. @@ -263,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: @@ -505,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 """ @@ -527,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. @@ -539,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. @@ -578,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. @@ -593,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. """ @@ -648,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 ) @@ -660,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. @@ -672,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. @@ -686,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. @@ -698,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. @@ -709,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. @@ -752,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` @@ -771,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. @@ -861,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. @@ -883,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. @@ -987,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]: @@ -999,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. @@ -1012,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. @@ -1048,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. @@ -1067,7 +1075,9 @@ def unpack_single(self, context: _ContextLike) -> _MemoryOT: if size is Ellipsis: return memoryview(stream.read()) - return memoryview(read_exact(context, size, "Memory field")) # pyright: ignore[reportReturnType] + return memoryview( + read_exact(context, size, "Memory field") + ) # pyright: ignore[reportReturnType] class Bytes(Memory[bytes, bytes]): @@ -1088,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. @@ -1119,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 @@ -1136,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. @@ -1195,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: """ @@ -1205,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) @@ -1232,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. @@ -1251,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. @@ -1283,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. @@ -1367,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") @@ -1449,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: @@ -1462,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. @@ -1474,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. @@ -1486,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. @@ -1500,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. @@ -1511,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. @@ -1572,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 @@ -1658,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. @@ -1670,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. @@ -1701,14 +1711,16 @@ def pack_single(self, obj: _PrefixIOT, context: _ContextLike) -> None: self.prefix.pack_single(len(obj), context) else: try: - self.prefix.__pack__(len(obj), context) # pyright: ignore[reportArgumentType] + 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. @@ -1794,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: @@ -1812,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. @@ -1822,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. @@ -1834,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" @@ -1854,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. @@ -1865,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", @@ -1940,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, @@ -1949,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 @@ -1973,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. @@ -1990,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. @@ -2010,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. @@ -2027,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. @@ -2041,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. @@ -2055,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. @@ -2080,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) @@ -2166,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. @@ -2177,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. @@ -2187,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. @@ -2229,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. @@ -2249,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. @@ -2261,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. @@ -2278,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) @@ -2330,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) @@ -2340,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) @@ -2349,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 @@ -2361,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") @@ -2371,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") @@ -2440,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 @@ -2449,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 """ @@ -2460,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 @@ -2469,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 """ @@ -2518,14 +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.struct: _StructLike[_IT, _OT] = ( + getstruct(struct) or struct + ) # pyright: ignore[reportAttributeAccessIssue] + 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 @@ -2537,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, @@ -2554,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" @@ -2569,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): @@ -2581,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) @@ -2593,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) @@ -2606,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) @@ -2620,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 @@ -2679,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, @@ -2694,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/varint.py b/src/caterpillar/fields/varint.py index 7ed0dc54..e979500c 100644 --- a/src/caterpillar/fields/varint.py +++ b/src/caterpillar/fields/varint.py @@ -19,15 +19,13 @@ from typing_extensions import override from caterpillar.abc import _ContextLike, _EndianLike, _StreamType -from caterpillar.byteorder import LITTLE_ENDIAN_FMT, O_DEFAULT_ENDIAN, LittleEndian +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 ._mixin import FieldStruct - -if typing.TYPE_CHECKING: - from caterpillar.fields import Field +from .common import resolve_order VARINT_LSB = Flag("varint.lsb") @@ -86,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 @@ -126,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/_base.py b/src/caterpillar/model/_base.py index 21b280f6..985cb3f3 100644 --- a/src/caterpillar/model/_base.py +++ b/src/caterpillar/model/_base.py @@ -40,7 +40,9 @@ CTX_SEQ, CTX_STREAM, O_CONTEXT_FACTORY, - Context, CTX_PARENT, CTX_ORDER, + Context, + CTX_PARENT, + CTX_ORDER, ) from caterpillar.exception import StructException, ValidationError from caterpillar.fields import INVALID_DEFAULT, Const, Field, FieldMixin @@ -49,6 +51,7 @@ S_DISCARD_UNNAMED, S_REPLACE_TYPES, S_UNION, + O_DEFAULT_STRUCT_ENDIAN, ) from caterpillar.shared import ATTR_ACTION_PACK, ATTR_ACTION_UNPACK, Action @@ -501,21 +504,30 @@ def __size__(self, context: _ContextLike) -> int: 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: - return None + resolved = O_DEFAULT_STRUCT_ENDIAN.value + if resolved is None: + return 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) + # 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 @@ -524,7 +536,9 @@ def _resolve_order(self, context: _ContextLike) -> _EndianLike | None: 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: + 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 @@ -589,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: @@ -603,7 +619,9 @@ 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: + 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 diff --git a/src/caterpillar/model/provider.py b/src/caterpillar/model/provider.py index e72357c6..a1dd6dba 100644 --- a/src/caterpillar/model/provider.py +++ b/src/caterpillar/model/provider.py @@ -24,9 +24,9 @@ from caterpillar.abc import ( _IT, _OT, - _ArchLike, + ArchLike, _ContainsStruct, - _EndianLike, + EndianLike, _StreamType, _SupportsPack, _SupportsSize, @@ -40,6 +40,7 @@ ) from caterpillar.context import CTX_STREAM, O_CONTEXT_FACTORY, Context from caterpillar.exception import DynamicSizeError +from caterpillar.options import O_DEFAULT_STRUCT_ENDIAN from caterpillar.shared import ATTR_PACK, MODE_PACK, MODE_UNPACK, getstruct, hasstruct @@ -51,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 @@ -65,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 @@ -78,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 @@ -92,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( @@ -104,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: """ @@ -136,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() @@ -150,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 @@ -164,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 @@ -178,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 @@ -192,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 @@ -206,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( @@ -219,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: """ @@ -302,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" @@ -360,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 @@ -371,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 @@ -385,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 @@ -399,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 @@ -413,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 @@ -427,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( @@ -440,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: """ @@ -473,6 +497,7 @@ def pack_file( order=order, arch=arch, fill=fill, + default_struct_order=default_struct_order, **kwds, ) @@ -484,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 @@ -495,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 @@ -506,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( @@ -515,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: """ @@ -569,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 @@ -588,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 @@ -599,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 @@ -610,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( @@ -620,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, ): """ @@ -634,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 index 07e3b2aa..84f373fc 100644 --- a/src/caterpillar/options.py +++ b/src/caterpillar/options.py @@ -18,7 +18,7 @@ from typing_extensions import Final, override -from caterpillar.abc import _VT, _ArrayFactoryLike, _OptionLike +from caterpillar.abc import _VT, _ArrayFactoryLike, _OptionLike, EndianLike @dataclass(init=False, eq=False) @@ -225,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") From c99b2278abdfbba2bb971b1754ce0c09e390bf70 Mon Sep 17 00:00:00 2001 From: MatrixEditor <58256046+MatrixEditor@users.noreply.github.com> Date: Sat, 22 Aug 2026 09:02:52 +0200 Subject: [PATCH 8/8] docs: add Inherit byteorder reference and tutorial --- docs/sphinx/source/library/byteorder.rst | 2 + .../protocols/byteorder_extensions.rst | 16 ++ .../source/tutorial/advanced/byteorder.rst | 215 +++++++++++++++++- src/caterpillar/model/_struct.py | 18 +- 4 files changed, 239 insertions(+), 12 deletions(-) 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/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/src/caterpillar/model/_struct.py b/src/caterpillar/model/_struct.py index 3a8719ab..6592f2f3 100644 --- a/src/caterpillar/model/_struct.py +++ b/src/caterpillar/model/_struct.py @@ -349,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 ``[]`` @@ -360,7 +360,7 @@ 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] @@ -370,13 +370,13 @@ def __set_byteorder__(cls, order: EndianLike) -> Field[Self, Self]: @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`` @@ -390,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 @@ -415,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)