Metadata-Version: 2.4
Name: construct-typing
Version: 0.8.1
Summary: Extension for the python package 'construct' that adds typing features
Author: Tim Riddermann
License-Expression: MIT
Project-URL: Homepage, https://github.com/timrid/construct-typing
Project-URL: Bug Reports, https://github.com/timrid/construct-typing/issues
Keywords: construct,kaitai,declarative,data structure,struct,binary,symmetric,parser,builder,parsing,building,pack,unpack,packer,unpacker,bitstring,bytestring,annotation,type hint,typing,typed,bitstruct,PEP 561
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Topic :: Software Development :: Build Tools
Classifier: Topic :: Software Development :: Code Generators
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Programming Language :: Python :: Implementation :: CPython
Classifier: Typing :: Typed
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: arrow>=1.0.0
Requires-Dist: construct==2.10.70
Requires-Dist: typing_extensions>=4.6.0
Dynamic: license-file

# construct-typing
[![PyPI](https://img.shields.io/pypi/v/construct-typing)](https://pypi.org/project/construct-typing/)
![PyPI - Implementation](https://img.shields.io/pypi/implementation/construct-typing)
![PyPI - Python Version](https://img.shields.io/pypi/pyversions/construct-typing)
![GitHub](https://img.shields.io/github/license/timrid/construct-typing)

This project is an extension of the python package [*construct*](https://pypi.org/project/construct/), which is a powerful **declarative** and **symmetrical** parser and builder for binary data. This Repository consists of two packages:

- **construct-stubs**: Adding .pyi for the whole *construct 2.10* package (according to  [PEP 561 stub-only packages](https://www.python.org/dev/peps/pep-0561/#stub-only-packages))
- **construct_typed**: Adding additional classes that help with autocompletion and additional type hints.

## Installation
This package complies with [PEP 561](https://www.python.org/dev/peps/pep-0561/). So most of the static code analysers will recognise the stubs automatically. The installation only requires:
```
pip install construct-typing
```

## Tests
The stubs are tested against the pytests of the *construct* package in a slightly modified form. Since the tests are relatively detailed I think most cases are covered.

The new typed constructs have new written pytests, which also passes all pytests and the static type checkers.

The following static type checkers are fully supported:
- [mypy](https://github.com/python/mypy)
- [pyright](https://github.com/microsoft/pyright)
- [ty](https://github.com/astral-sh/ty) (experimental, since ty itself is still in development)

## Development

This project uses `uv` as a project management tool. To set up your development environment, run the following command:

```bash
uv sync
```

To run the unit tests, run:

```bash
uv run poe test
```

To run the linter/code formatter (including auto fix), run:

```bash
uv run poe lint
```

To run all supported type checkers, run:

```bash
uv run poe typecheck
```

To run unit tests, linter/code formatter and type checkers, run:

```bash
uv run poe check-all
```

## Explanation
### Stubs
The **construct-stubs** package is used for creating type hints for the orignial *construct* package. In particular the `build` and `parse` methods get type hints. So the core of the stubs  are the `TypeVar`'s `ParsedType` and `BuildTypes`:
- `Construct.build`: converts an object of one of the types defined by `BuildTypes` to a `bytes` object.
- `Construct.parse`: converts a `bytes` object to an object of type `ParsedType`.

For each `Construct` the stub file defines to which type it parses to and from which it can be build. For example:

| Construct            | parses to (ParsedType)         | builds from (BuildTypes)                 |
| -------------------- | ------------------------------ | ---------------------------------------- |
| `Int16ub`            | `int`                          | `int`                                    |
| `Bytes`              | `bytes`                        | `bytes`, `bytearray` or `memoryview`     |
| `Array(5, Int16ub)`  | `ListContainer[int]`           | `typing.List[int]`                       |
| `Struct("i" / Byte)` | `Container[typing.Any]`        | `typing.Dict[str, typing.Any]` or `None` |

The problem is to describe the more complex constructs like:
 - `Sequence`, `FocusedSeq` which has heterogenous subcons in comparison to an `Array` with only homogenous subcons. 
 - `Struct`, `BitStruct`, `LazyStruct`, `Union` which has heterogenous and named subcons.

Currently only the very unspecific type `typing.Any` can be used as type hint (maybe in the future it can be optimised a little, when [variadic generics](https://mail.python.org/archives/list/typing-sig@python.org/thread/SQVTQYWIOI4TIO7NNBTFFWFMSMS2TA4J/) become available). But the biggest disadvantage is that autocompletion for the named subcons is not available.

Note: The stubs are based on *construct* in Version 2.10.


### Typed
**!!! EXPERIMENTAL VERSION !!!**

To include autocompletion and further enhance the type hints for these complex constructs the **construct_typed** package is used as an extension to the original *construct* package. It is mainly a few Adapters with the focus on type hints.

It implements the following new constructs:
- `DataclassStruct`: similar to `construct.Struct` but strictly tied to `DataclassMixin` and `@dataclasses.dataclass`
- `DataclassBitStruct`: similar to `construct.BitStruct` but strictly tied to `DataclassMixin` and `@dataclasses.dataclass`
- `TEnum`: similar to `construct.Enum` but strictly tied to a `TEnumBase` class
- `TFlagsEnum`: similar to `construct.FlagsEnum` but strictly tied to a `TFlagsEnumBase` class

These types are strongly typed, which means that there is no difference between the `ParsedType` and the `BuildTypes`. So to build one of the constructs the correct type is enforced. The disadvantage is that the code will be a little bit longer, because you can not for example use a normal `dict` to build an `DataclassStruct`. But the big advantage is, that if you use the correct container type instead of a `dict`, the static code analyses can do its magic and find potential type errors and missing values without running the code itself.


A short example:

```python
import dataclasses
import typing as t
from construct import Array, Byte, Bytes, Int8ub, this
from construct_typed import DataclassMixin, DataclassStruct, EnumBase, TEnum, csfield, csfield_const

class Orientation(EnumBase):
    HORIZONTAL = 0
    VERTICAL = 1

@dataclasses.dataclass
class Image(DataclassMixin):
    signature: bytes = csfield_const(Bytes(3), b"BMP")
    orientation: Orientation = csfield(TEnum(Int8ub, Orientation))
    width: int = csfield(Int8ub)
    height: int = csfield(Int8ub)
    pixels: t.List[int] = csfield(Array(this.width * this.height, Byte))

format = DataclassStruct(Image)
obj = Image(
    orientation=Orientation.VERTICAL,
    width=3,
    height=2,
    pixels=[7, 8, 9, 11, 12, 13],
)
print(format.build(obj))
print(format.parse(b"BMP\x01\x03\x02\x07\x08\t\x0b\x0c\r"))
```
Output:
```
b'BMP\x01\x03\x02\x07\x08\t\x0b\x0c\r'
Image: 
    signature = b'BMP' (total 3)
    orientation = 1
    width = 3
    height = 2
    pixels = ListContainer: 
        7
        8
        9
        11
        12
        13
```

#### Using constants in DataclassStruct
If you want a simple, fixed constant in a `DataclassStruct`, use `csfield_const`. It automatically wraps the given value in a `cs.Const` construct and sets the value directly, without a constructor parameter, when the `DataclassStruct` instance is created.

```python
import dataclasses
from construct import Int8ub, Bytes
from construct_typed import DataclassMixin, csfield, csfield_const
import inspect


@dataclasses.dataclass
class Image(DataclassMixin):
    signature: bytes = csfield_const(Bytes(3), b"BMP")  # <-- no constructor parameter is generated
    width: int = csfield(Int8ub)
    height: int = csfield(Int8ub)


print(inspect.signature(Image))  # -> (width: int, height: int) -> None
```

#### Using defaults in DataclassStruct
If you want a simple, fixed default value for a parameter in a `DataclassStruct`, use `csfield_default`. It automatically wraps the given value in a `cs.Default` construct and sets it as the field's default value. This default value can still be overridden through the `DataclassStruct` constructor. As a consequence, all fields that follow it in the constructor must be marked as `kw_only`.

```python
import dataclasses
from construct import Int8ub, Bytes
from construct_typed import DataclassMixin, csfield, csfield_default
import inspect


@dataclasses.dataclass
class Image(DataclassMixin):
    some_value: int = csfield(Int8ub)
    signature: bytes = csfield_default(Bytes(3), default=b"BMP")  # <-- constructor parameter is generated with default value b"BMP"
    width: int = csfield(Int8ub, kw_only=True)  # <-- kw_only is required for all fields after a default field
    height: int = csfield(Int8ub, kw_only=True)  # <-- kw_only is required for all fields after a default field


print(inspect.signature(Image))  # -> (some_value: int, signature: bytes = b'BMP', *, width: int, height: int) -> None
```

For more complex cases, where the default value needs to be computed dynamically from the context, use `csfield_noinit(Default(...))` instead, since the context is not yet known when the `DataclassStruct` instance is created. For example:

```python
import dataclasses
from construct import Int8ub, Int16ub, Default, this
from construct_typed import DataclassMixin, csfield, csfield_noinit
import inspect


@dataclasses.dataclass
class Image(DataclassMixin):
    width: int = csfield(Int8ub)
    height: int = csfield(Int8ub)
    min_buffer_size: int | None = csfield_noinit(  # <-- "| None" is required
        Default(Int16ub, this.width * this.height)
    )

print(inspect.signature(Image))  # -> (width: int, height: int) -> None
```



#### Using generic constructs that build from `None`
Some constructs, such as `cs.Computed`, `cs.Rebuild`, `cs.Padding`, `cs.Tell`, `cs.Pass` and `cs.Terminated`, are only computed dynamically at parse/build time. Fields for these constructs must be declared with `csfield_noinit`, because their values are not yet known when a `DataclassStruct` instance is created through the constructor, and are therefore automatically initialized to `None`. As a result, these fields cannot be overridden through the constructor. Because the constructor always sets them to `None`, their type must always be `<type> | None`.

```python
import dataclasses
from construct import Int8ub, Computed
from construct.core import Tell
from construct_typed import DataclassMixin, csfield, csfield_noinit
import inspect


@dataclasses.dataclass
class Image(DataclassMixin):
    width: int = csfield(Int8ub)
    height: int = csfield(Int8ub)
    pos: int | None = csfield_noinit(Tell)  # <-- "| None" is required
    size: int | None = csfield_noinit(  # <-- "| None" is required
        Computed(lambda ctx: ctx.width * ctx.height)
    )

print(inspect.signature(Image))  # -> (width: int, height: int) -> None
```

Note: `csfield_noinit` can technically also be used with `cs.Const` or a non-lambda `cs.Default`, but the field then stays `None` until the struct is parsed, since the value is not assigned by the constructor. If the value should already be available right after construction, use `csfield_const()` or `csfield_default()` instead.
