Skip to content

Commit 5dd9e5f

Browse files
authored
feat!: add new feature "inline-syntax"
- New *inline* syntax using the `<<` operator to automatically unpack structs from data - All default struct types now implement `from_bytes`, `from_file` and `to_bytes` for direct packing or unpacking - Add `PackMixin` and `UnpackMixin` to automatically implement the methods mentioned above for struct types - `pack`, `unpack` and `sizeof` were moved into their own module (no changed visibe when importing from `caterpillar.models`) - Drop support for `'x'` format character in `PyStructFormattedField` - Pre-computed conditions in the `Field` class incorrectly evaluated to true or false unconditionally (in case of a context-lambda). This issue has now been fixed in all places. - Fix some missing or wrong typing annotations Merge pull request #59 from MatrixEditor/feat/inline-syntax
2 parents 2d244a7 + c780971 commit 5dd9e5f

45 files changed

Lines changed: 1280 additions & 705 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

docs/sphinx/source/library/fields/common.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ Numeric Structs
1313
.. versionchanged:: 2.4.0
1414
:code:`FormatField` renamed to :code:`PyStructFormattedField`
1515

16+
.. versionchanged:: 2.8.1
17+
Dropped support for format character ``'x'``. Use the :class:`Padding` type instead.
18+
1619
.. data:: caterpillar.fields.uint8
1720

1821
Unsigned 8-bit integer field. Range: ``0`` to ``255``.

docs/sphinx/source/tutorial/first_steps/extended_syntax.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
Extended Syntax
44
===============
55

6+
.. versionadded:: 2.8.0
7+
68
The extended syntax introduces a declarative and type-checker-friendly way to
79
define structs using ordinary Python classes. Instead of constructing structures
810
through procedural builder calls, you describe the format directly through type

docs/sphinx/source/tutorial/first_steps/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ imported the necessary components from *Caterpillar*:
4646
structdef
4747
extended_syntax
4848
parsing
49+
inline_syntax
4950
configuration
5051
documentation
5152

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
.. _first_steps-inline-syntax:
2+
3+
Inline Syntax
4+
=============
5+
6+
In addition to the standard methods for packing and unpacking data, *Caterpillar* also
7+
supports an **inline syntax** feature that allows for more concise and expressive data
8+
parsing. This syntax uses Python's overloaded left-shift operator (``<<``) to easily
9+
unpack data from a bytes object or a stream, making the code both compact and readable.
10+
11+
Unpacking Data Inline
12+
^^^^^^^^^^^^^^^^^^^^^^
13+
14+
With the inline syntax, you can unpack binary data into a struct directly using the
15+
left-shift operator. This operator is a shorthand for calling the ``from_bytes`` method,
16+
which means you don't need to explicitly invoke a function to parse your data.
17+
18+
.. code-block:: python
19+
20+
from io import BytesIO
21+
from caterpillar.py import uint8
22+
23+
# data can be unpacked INLINE using a special operator
24+
data = b"\x00\x00\x01\xff"
25+
26+
# If using on a static bytes object, the struct will always begin at offset zero
27+
value1 = uint8 << data
28+
value2 = uint8 << data
29+
assert value1 == value2
30+
31+
32+
In the example above, the binary data is unpacked into the :data:`~caterpillar.fields.uint8` struct.
33+
The left-shift operator (``<<``) reads the data starting at the beginning of the byte stream and
34+
automatically handles the parsing for you.
35+
36+
When using a stream, the operator will start unpacking from the current position in the stream. This
37+
is useful for processing data incrementally or when you want to track your stream's current position
38+
manually.
39+
40+
>>> stream = BytesIO(data)
41+
>>> stream.seek(2) # Move the stream position to byte index 2
42+
>>> uint8 << stream
43+
1
44+
45+
Wrapper methods
46+
^^^^^^^^^^^^^^^
47+
48+
Instead of using the special operator, all struct classes in *Caterpillar* also provide the typical
49+
wrapper functions for packing and unpacking:
50+
51+
>>> # Instead of using the special operator, all default struct classes provide
52+
>>> # wrapper functions for packing and unpacking:
53+
>>> uint8.from_bytes(data)
54+
0
55+
>>> uint8.to_bytes(0xFF)
56+
b"\0xFF"
57+
58+
This inline syntax feature is great for simplifying your code when dealing with binary data, making the
59+
code both more readable and intuitive.

docs/sphinx/source/tutorial/first_steps/parsing.rst

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,4 +36,34 @@ RGB(r=1, g=2, b=3)
3636

3737
Now that you've seen how to define, pack, and unpack data with structs in *Caterpillar*, you're
3838
almost ready to start working with more complex data structures. And remember,
39-
we've just scratched the surface—there's a lot more to explore!
39+
we've just scratched the surface—there's a lot more to explore!
40+
41+
.. tip::
42+
43+
.. versionadded:: 2.8.1
44+
45+
You can use a special *mixin* class to add wrapper methods to your struct
46+
class:
47+
48+
.. code-block:: python
49+
50+
@struct
51+
class RGB(struct_factory.mixin):
52+
...
53+
54+
It will then be possible to use the type directly instead of importing
55+
:func:`~caterpillar.model.pack` or :func:`~caterpillar.model.unpack`
56+
every time.
57+
58+
>>> obj = RGB.from_bytes(b"\x01\x02\x03")
59+
RGB(r=1, g=2, b=3)
60+
>>> obj.to_bytes()
61+
b"\x01\x02\x03"
62+
63+
The same applies to all common structs mentioned in the next chapter
64+
and :class:`~caterpillar.fields.Field` objects.
65+
66+
>>> uint8[3].from_bytes(b"\x01\x02\x03")
67+
[1,2,3]
68+
>>> uint8[3].to_bytes([1,2,3])
69+
b"\x01\x02\x03"

examples/formats/caf.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Copyright (C) MatrixEditor 2023-2025
1+
# Copyright (C) MatrixEditor 2023-2026
22
#
33
# This program is free software: you can redistribute it and/or modify
44
# it under the terms of the GNU General Public License as published by

examples/formats/itdb.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Copyright (C) MatrixEditor 2023-2025
1+
# Copyright (C) MatrixEditor 2023-2026
22
#
33
# This program is free software: you can redistribute it and/or modify
44
# it under the terms of the GNU General Public License as published by

examples/formats/nibarchive.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Copyright (C) MatrixEditor 2023-2025
1+
# Copyright (C) MatrixEditor 2023-2026
22
#
33
# This program is free software: you can redistribute it and/or modify
44
# it under the terms of the GNU General Public License as published by

examples/inline_syntax.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
from io import BytesIO
2+
from caterpillar.py import uint8
3+
4+
# data can be unpacked INLINE using a special operator
5+
data = b"\x00\x00\x01\xff"
6+
7+
# If using on a static bytes object, the struct will always begin at offset zero
8+
value = uint8 << data
9+
other_value = uint8 << data
10+
assert value == other_value
11+
12+
# When using a stream, the current stream position will be modified
13+
stream = BytesIO(data)
14+
_ = stream.seek(2)
15+
value = uint8 << stream
16+
other_value = uint8 << stream
17+
assert value != other_value
18+
19+
# Instead of using the special operator, all default struct classes provide
20+
# wrapper functions for packing and unpacking:
21+
value = uint8.from_bytes(data)
22+
assert uint8.to_bytes(value) == data[:1]

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ cmake.source-dir = "."
88

99
[project]
1010
name = "caterpillar"
11-
version = "2.8.0"
11+
version = "2.8.1rc"
1212
requires-python = ">=3.10"
1313
description = "Library to pack and unpack structurized binary data."
1414
authors = [{ name = "MatrixEditor" }]

0 commit comments

Comments
 (0)