Skip to content

Split csfield into csfield, csfield_noinit, csfield_const and csfield_default#41

Open
timrid wants to merge 2 commits into
mainfrom
split-csfield-in-multiple-csfields
Open

Split csfield into csfield, csfield_noinit, csfield_const and csfield_default#41
timrid wants to merge 2 commits into
mainfrom
split-csfield-in-multiple-csfields

Conversation

@timrid

@timrid timrid commented Jul 17, 2026

Copy link
Copy Markdown
Owner

Problem

Previously, there was only a single csfield method to define dataclass fields for DataclassStruct. This led to ambiguity and potentially incorrect behavior:

  • It wasn't clear whether a field required a constructor parameter, had a fixed default value, should be excluded from the constructor entirely, or represented a constant value.
  • Constructs that are computed dynamically at parse/build time (e.g. Computed, Rebuild, Tell, Const) had to be declared via csfield, even though they can't provide a meaningful value when the instance is created — this resulted in unnecessary or incorrectly typed constructor parameters.

Solution

csfield has been split into four specialized methods that make the intended behavior explicit:

  • csfield – still used for constructs that cannot build from None (the default case, generates a required constructor parameter).
  • csfield_noinit – for fields excluded from the constructor (e.g. Computed, Rebuild, Tell); the value is automatically set to None on construction, so the field type must be T | None.
  • csfield_const – automatically wraps the value in cs.Const and sets it directly, without a constructor parameter:
    signature: bytes = csfield_const(Bytes(3), b"BMP")
  • csfield_default – wraps the value in cs.Default and generates a constructor parameter with a default value (all following fields must be kw_only):
    signature: bytes = csfield_default(Bytes(3), default=b"BMP")

Additional changes

  • Type stubs for Checksum were optimized to correctly reflect whether the respective construct can build from None.

Breaking Changes

  • csfield should now only be used for constructs that cannot build from None. For every other case, use csfield_noinit, csfield_const or csfield_default.

(This pull request replaces #38)

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR makes DataclassStruct dataclass field intent explicit by splitting the former single csfield helper into four dedicated field specifiers (csfield, csfield_noinit, csfield_const, csfield_default), improving runtime behavior clarity and enabling more accurate static type checking (notably with Pyright). It also updates tests, documentation, and stubs to reflect the new APIs and correct build-from-None semantics.

Changes:

  • Introduces new field-specifier helpers and applies @dataclass_transform to improve type-checker understanding of generated dataclass constructors.
  • Expands/adjusts tests (including a new Pyright-focused test module) to validate constructor parameter behavior and validation errors.
  • Updates README/CHANGELOG and improves stubs (notably Checksum) to reflect build(None) capability.

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
construct_typed/dataclass_struct.py Splits field creation into four helpers + adds dataclass_transform field specifiers.
construct_typed/__init__.py Exports the new field helpers from the package API surface.
construct-stubs/core.pyi Adjusts Checksum build typing to allow None builds.
tests/test_typed.py Updates/extends runtime tests for the new field behaviors and validation.
tests/test_typed_pyright.py Adds Pyright regression tests for missing/extraneous dataclass constructor parameters.
README.md Updates examples and adds new sections documenting the new helpers.
pyproject.toml Excludes the Pyright-specific test from mypy to avoid intentional error noise.
CHANGELOG.md Documents breaking changes and new helper APIs.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread README.md
@@ -101,15 +101,15 @@ A short example:
import dataclasses
import typing as t
from construct import Array, Byte, Const, Int8ub, this
Comment thread README.md

```python
import dataclasses
from construct import Const, Int8ub, Bytes
Comment thread CHANGELOG.md
Comment on lines +4 to +5
** Breaking changes: **
- `csfield` should only used for constructs that cannot build from `None`. Every other construct should use the new `csfield_noinit`, `csfield_const` or `csfield_default`.
Comment thread CHANGELOG.md
- Removed `version.py`, use `importlib.metadata` instead to get the version number.
- Use `uv` as a project management tool and `poe` as a task runner.

** New features: **
Comment thread CHANGELOG.md
- Removed wildcard imports, so it may be necessary to change some import statements in your code when you use non-public APIs. No newline at end of file
- Optimize type stubs for `Checksum`, to represent that it can build from `None`.

** Organizational changes: **
Comment thread tests/test_typed.py
# Construct allows to put non-default values after Default. Dataclass and Pyright don't like that too much. It is necessary to
# specify the field `kw_only` and pass it "by keyword".
normal_int: int = csfield(cs.Int8ub, kw_only=True)
const_int2: int | None = csfield_const(cs.Int8ub, 5)
Comment on lines +133 to +137
return dataclasses.field(
default=const,
init=False,
metadata={"subcon": subcon},
)
Comment on lines +176 to +180
return dataclasses.field(
default=default,
init=True,
metadata={"subcon": subcon},
)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants