Skip to content

Releases: doronz88/bytestruct

v0.2.0

Choose a tag to compare

@doronz88 doronz88 released this 18 Oct 06:00
1ba30f4

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

Full Changelog: v0.1.2...v0.2.0

v0.1.2

Choose a tag to compare

@doronz88 doronz88 released this 17 Oct 21:03
7525555

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

Full Changelog: v0.1.1...v0.1.2

v0.1.1

Choose a tag to compare

@doronz88 doronz88 released this 17 Oct 14:51
6776f3d

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

Full Changelog: v0.1.0...v0.1.1

v0.1.0

Choose a tag to compare

@doronz88 doronz88 released this 17 Oct 10:58

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.