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