Repository navigation
Releases: doronz88/bytestruct
Release list
v0.2.0
Highlights
✨ Parsing from streams
Every codec with parse (records, scalars and the combinators) now has parse_stream(read) and parse_stream_async(read), which parse one value from a file, a socket or an asyncio stream and leave the stream right after it. The value is parsed from the bytes read so far, starting with its minimum size, and exactly the missing bytes are read until it is complete, so nothing after the value is consumed. TruncatedDataError gains a needed attribute for this.
message = await Message.parse_stream_async(reader.readexactly)
header = Header.parse_stream(f.read)It is slower than reading the bytes by hand and calling parse, since a value with a variable part is parsed at least twice: 1.97 µs instead of 0.20 µs for a 6-byte header and a 16-byte body. See Streams for the rules and measurements.
Existing parsing is unchanged, both in API and in speed.
📚 Logo and social card
The documentation site and README have a project logo, and links to the site show a social card.
What's Changed
- 40359fe docs: Document that parse_stream is slower than parsing by hand (#5) (@doronz88)
- 1ae8b63 bytestruct: Make parse_stream a method of every codec with parse (#5) (@doronz88)
- 3980aa6 bytestruct: Parse values from streams (#5) (@doronz88)
- c469bdd docs: Add a social card and Open Graph tags (#4) (@doronz88)
- 8852b19 docs: Add a project logo (#3) (@doronz88)
Full Changelog: v0.1.2...v0.2.0
v0.1.2
Highlights
⚡ Faster records
Each record class now gets its own parse, read and build, compiled when the class is defined, so calls no longer work out on each call what the record needs. A record with a Const magic parses in 0.14 µs instead of 0.29 µs, the same as hand-written struct.unpack_from into a dataclass, and builds in 0.10 µs instead of 0.16 µs. Other record shapes are 1.2-2.6x faster, with the largest gains for records with padding or enum fields. Enum fields now decode with a member lookup instead of calling the enum. The benchmark now includes a ctypes column.
The API, results and errors are unchanged.
🐛 @dataclass(slots=True) records
Records declared with slots=True failed on every parse, because the recreated class treated init=False (constant and padding) fields as __init__ parameters. They work now.
What's Changed
- d56db7e bytestruct: Give each record its own parse, read and build (#2) (@doronz88)
- 6c111ad bytestruct: Decode enum fields with a member lookup (#2) (@doronz88)
- 87854bb bytestruct: Keep init=False fields out of init in slots=True records (#2) (@doronz88)
Full Changelog: v0.1.1...v0.1.2
v0.1.1
Highlights
📚 Contributor guide and documented internals
A new CONTRIBUTING.md covers where things are, setting up with uv, the checks CI runs, building the docs and how releases are cut. The source now has a section map after the imports and docstrings on the record compiler (__init_subclass__, _layout, _build_fixed), pointing to How it works.
⚡ Performance numbers in the README
The README now shows the construct benchmark: bytestruct is 4-13x faster than construct and 3-7x faster than compiled construct.
No code changes since 0.1.0.
What's Changed
- 9022d5a bytestruct: Document the module sections and the record compiler (#1) (@doronz88)
- f36feab docs: Show the construct benchmark in the README (#1) (@doronz88)
- 88d53c6 docs: Add a contributor guide (#1) (@doronz88)
Full Changelog: v0.1.0...v0.1.1
v0.1.0
First release of bytestruct: typed, dependency-free binary records and codecs on top of struct, extracted from pymobiledevice3 as its construct replacement.
Highlights
✨ Declarative fixed-size records
Dataclasses deriving from BinaryStruct, with each field's wire type given by its annotation: integers and floats in any byte order, Bytes, PaddedString, uuid_t, ipv4/ipv6, Array, int-enum fields and Const magics. The layout compiles into struct.Struct once, when the class is defined: 7x faster than construct to parse a header and 13x faster to build one.
@dataclass
class Header(BinaryStruct, byte_order="<"):
magic: Annotated[u32, Const(0x1F3D5B79)] = field(default=0x1F3D5B79, init=False)
size: u32
name: Annotated[str, PaddedString(16)]✨ Composable codecs for variable-length data
Prefixed, PrefixedArray, Switch, Aligned, Tuple, CSTRING, GREEDY_STRING, GREEDY_BYTES and the full set of scalars compose like construct's. Records are codecs too, and ByteReader covers formats with no declarative shape.
✨ Located errors
BinaryParseError/BinaryBuildError (both ValueErrors) name the record, field, path and absolute offset of the failure, e.g. DICTIONARY -> [2] -> STRING at offset 104: expected 200 bytes, found 16.
📚 Documentation
Guides, a cookbook, an extending guide and a full type reference with a round trip for every public name: https://doronz88.github.io/bytestruct/
Python 3.9+, pyright-clean, MIT licensed.