Compare commits

...

5 commits
v0.7.0 ... main

Author SHA1 Message Date
wrapper
486c553d65 fallback switch
Some checks failed
CI / OS ubuntu-latest, Python 3.10 (push) Has been cancelled
CI / OS ubuntu-latest, Python 3.11 (push) Has been cancelled
CI / OS ubuntu-latest, Python 3.12 (push) Has been cancelled
CI / OS ubuntu-latest, Python 3.13 (push) Has been cancelled
CI / OS ubuntu-latest, Python 3.9 (push) Has been cancelled
CI / OS windows-latest, Python 3.10 (push) Has been cancelled
CI / OS windows-latest, Python 3.11 (push) Has been cancelled
CI / OS windows-latest, Python 3.12 (push) Has been cancelled
CI / OS windows-latest, Python 3.13 (push) Has been cancelled
CI / OS windows-latest, Python 3.9 (push) Has been cancelled
CI / create_wheel_and_sdist (push) Has been cancelled
2026-04-07 23:28:33 +07:00
wrapper
0a4628935b add
Some checks are pending
CI / OS ubuntu-latest, Python 3.10 (push) Waiting to run
CI / OS ubuntu-latest, Python 3.11 (push) Waiting to run
CI / OS ubuntu-latest, Python 3.12 (push) Waiting to run
CI / OS ubuntu-latest, Python 3.13 (push) Waiting to run
CI / OS ubuntu-latest, Python 3.9 (push) Waiting to run
CI / OS windows-latest, Python 3.10 (push) Waiting to run
CI / OS windows-latest, Python 3.11 (push) Waiting to run
CI / OS windows-latest, Python 3.12 (push) Waiting to run
CI / OS windows-latest, Python 3.13 (push) Waiting to run
CI / OS windows-latest, Python 3.9 (push) Waiting to run
CI / create_wheel_and_sdist (push) Waiting to run
2026-04-07 20:19:38 +07:00
wrapper
c1896ab8dc Construct Error class does not return
Some checks are pending
CI / OS ubuntu-latest, Python 3.10 (push) Waiting to run
CI / OS ubuntu-latest, Python 3.11 (push) Waiting to run
CI / OS ubuntu-latest, Python 3.12 (push) Waiting to run
CI / OS ubuntu-latest, Python 3.13 (push) Waiting to run
CI / OS ubuntu-latest, Python 3.9 (push) Waiting to run
CI / OS windows-latest, Python 3.10 (push) Waiting to run
CI / OS windows-latest, Python 3.11 (push) Waiting to run
CI / OS windows-latest, Python 3.12 (push) Waiting to run
CI / OS windows-latest, Python 3.13 (push) Waiting to run
CI / OS windows-latest, Python 3.9 (push) Waiting to run
CI / create_wheel_and_sdist (push) Waiting to run
2026-04-07 19:25:45 +07:00
wrapper
2f078b340b more switch fixes
Some checks are pending
CI / OS ubuntu-latest, Python 3.10 (push) Waiting to run
CI / OS ubuntu-latest, Python 3.11 (push) Waiting to run
CI / OS ubuntu-latest, Python 3.12 (push) Waiting to run
CI / OS ubuntu-latest, Python 3.13 (push) Waiting to run
CI / OS ubuntu-latest, Python 3.9 (push) Waiting to run
CI / OS windows-latest, Python 3.10 (push) Waiting to run
CI / OS windows-latest, Python 3.11 (push) Waiting to run
CI / OS windows-latest, Python 3.12 (push) Waiting to run
CI / OS windows-latest, Python 3.13 (push) Waiting to run
CI / OS windows-latest, Python 3.9 (push) Waiting to run
CI / create_wheel_and_sdist (push) Waiting to run
2026-04-07 19:02:24 +07:00
wrapper
0c93e4d551 mod
Some checks are pending
CI / OS ubuntu-latest, Python 3.10 (push) Waiting to run
CI / OS ubuntu-latest, Python 3.11 (push) Waiting to run
CI / OS ubuntu-latest, Python 3.12 (push) Waiting to run
CI / OS ubuntu-latest, Python 3.13 (push) Waiting to run
CI / OS ubuntu-latest, Python 3.9 (push) Waiting to run
CI / OS windows-latest, Python 3.10 (push) Waiting to run
CI / OS windows-latest, Python 3.11 (push) Waiting to run
CI / OS windows-latest, Python 3.12 (push) Waiting to run
CI / OS windows-latest, Python 3.13 (push) Waiting to run
CI / OS windows-latest, Python 3.9 (push) Waiting to run
CI / create_wheel_and_sdist (push) Waiting to run
2026-04-07 18:48:27 +07:00
8 changed files with 119 additions and 54 deletions

3
.gitignore vendored
View file

@ -129,3 +129,6 @@ dmypy.json
example_737
example_888
example_ksy.ksy
# Test stuff
devtest/

View file

@ -1,3 +1,12 @@
## Modified version of "construct-typing" module used in my projects.
This modification features:
- **[EnhancedDataclassMixin](https://github.com/waszil/construct-typing/commit/479b51344bfd95149596a75ee574ac2e63c032df) with additional features**
- **ConstantOrContextLambda2 type**
- **Typing for Subconstruct**
- **Type hint for Computed**
- **Switch typing fixes**
The original README.md file was described down below:
# 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)

View file

@ -622,7 +622,7 @@ class Check(Construct[None, None]):
func: ConstantOrContextLambda[bool],
) -> None: ...
Error: Construct[None, None]
Error: Construct[t.NoReturn, t.NoReturn]
class FocusedSeq(Construct[t.Any, t.Any]):
subcons: t.List[Construct[t.Any, t.Any]]
@ -800,6 +800,10 @@ def If(
) -> IfThenElse[t.Optional[ThenParsedType], t.Optional[ThenBuildTypes]]: ...
SwitchType = t.TypeVar("SwitchType")
SwitchParsedType = t.TypeVar("SwitchParsedType")
SwitchBuildTypes = t.TypeVar("SwitchBuildTypes")
SwitchDefaultParsedType = t.TypeVar("SwitchDefaultParsedType")
SwitchDefaultBuildTypes = t.TypeVar("SwitchDefaultBuildTypes")
class Switch(Construct[ParsedType, BuildTypes]):
keyfunc: ConstantOrContextLambda[t.Any]
@ -807,17 +811,31 @@ class Switch(Construct[ParsedType, BuildTypes]):
default: Construct[t.Any, t.Any]
@t.overload
def __new__(
cls: "type[Switch[int, t.Optional[int]]]",
cls: "type[Switch[SwitchParsedType | None, SwitchBuildTypes | None]]",
keyfunc: ConstantOrContextLambda[SwitchType],
cases: t.Dict[SwitchType, Construct[int, int]],
default: t.Optional[Construct[int, int]] = ...,
) -> Switch[int, t.Optional[int]]: ...
cases: dict[t.Any, Construct[SwitchParsedType, SwitchBuildTypes]],
default: None = ...,
) -> Switch[SwitchParsedType | None, SwitchBuildTypes | None]: ...
@t.overload
def __new__(
cls: "type[Switch[SwitchParsedType, SwitchBuildTypes]]",
keyfunc: ConstantOrContextLambda[SwitchType],
cases: dict[t.Any, Construct[SwitchParsedType, SwitchBuildTypes]],
default: Construct[t.NoReturn, t.NoReturn],
) -> Switch[SwitchParsedType, SwitchBuildTypes]: ...
@t.overload
def __new__(
cls: "type[Switch[SwitchParsedType | SwitchDefaultParsedType, SwitchBuildTypes | SwitchDefaultBuildTypes]]",
keyfunc: ConstantOrContextLambda[SwitchType],
cases: dict[t.Any, Construct[SwitchParsedType, SwitchBuildTypes]],
default: Construct[SwitchDefaultParsedType, SwitchDefaultBuildTypes],
) -> Switch[SwitchParsedType | SwitchDefaultParsedType, SwitchBuildTypes | SwitchDefaultBuildTypes]: ...
@t.overload
def __new__(
cls: "type[Switch[t.Any, t.Any]]",
keyfunc: ConstantOrContextLambda[t.Any],
cases: t.Dict[t.Any, Construct[t.Any, t.Any]],
default: t.Optional[Construct[t.Any, t.Any]] = ...,
keyfunc: ConstantOrContextLambda[SwitchType],
cases: dict[t.Any, Construct[t.Any, t.Any]],
default: Construct[t.Any, t.Any] | None = ...,
) -> Switch[t.Any, t.Any]: ...
class StopIf(Construct[None, None]):

View file

@ -9,15 +9,19 @@ from .dataclass_struct import (
TStructField,
csfield,
sfield,
EnhancedDataclassMixin
)
from .generic_wrapper import (
Adapter,
ConstantOrContextLambda,
ConstantOrContextLambda2,
Construct,
Context,
ListContainer,
PathType,
Array
Array,
Subconstruct,
Computed,
)
from .tenum import EnumBase, EnumValue, FlagsEnumBase, TEnum, TFlagsEnum
@ -32,6 +36,7 @@ __all__ = [
"TStructField",
"csfield",
"sfield",
"EnhancedDataclassMixin",
"EnumBase",
"EnumValue",
"FlagsEnumBase",
@ -39,9 +44,12 @@ __all__ = [
"TFlagsEnum",
"Adapter",
"ConstantOrContextLambda",
"ConstantOrContextLambda2",
"Construct",
"Context",
"ListContainer",
"PathType",
"Array"
"Array",
"Subconstruct",
"Computed"
]

View file

@ -1,5 +1,6 @@
# -*- coding: utf-8 -*-
# pyright: strict
# pyright: reportIncompatibleVariableOverride=false, reportAny=false
import dataclasses
import textwrap
import typing as t
@ -11,6 +12,7 @@ from construct.lib.containers import (
recursion_lock,
)
from construct.lib.py3compat import bytestringtype, reprstring, unicodestringtype
from typing_extensions import override
from .generic_wrapper import Adapter, Construct, Context, ParsedType, PathType
@ -27,7 +29,7 @@ class DataclassMixin:
methods exists and every name can be used.
"""
__dataclass_fields__: "t.ClassVar[t.Dict[str, dataclasses.Field[t.Any]]]"
__dataclass_fields__: "t.ClassVar[dict[str, dataclasses.Field[t.Any]]]"
def __getitem__(self, key: str) -> t.Any:
return getattr(self, key)
@ -77,8 +79,8 @@ class DataclassMixin:
def csfield(
subcon: Construct[ParsedType, t.Any],
doc: t.Optional[str] = None,
parsed: t.Optional[t.Callable[[t.Any, Context], None]] = None,
doc: str | None = None,
parsed: t.Callable[[t.Any, Context], None] | None = None,
) -> ParsedType:
"""
Helper method for "DataclassStruct" and "DataclassBitStruct" to create the dataclass fields.
@ -155,15 +157,11 @@ class DataclassStruct(Adapter[t.Any, t.Any, DataclassType, DataclassType]):
subcon: "cs.Struct" # type: ignore
def __init__(
self,
dc_type: t.Type[DataclassType],
dc_type: type[DataclassType],
reverse: bool = False,
) -> None:
if not issubclass(dc_type, DataclassMixin): # type: ignore
raise TypeError(f"'{repr(dc_type)}' has to be a '{repr(DataclassMixin)}'")
if not dataclasses.is_dataclass(dc_type):
raise TypeError(f"'{repr(dc_type)}' has to be a 'dataclasses.dataclass'")
self.dc_type = dc_type
self.reverse = reverse
self.dc_type: type[DataclassType] = dc_type
self.reverse: bool = reverse
# get all fields from the dataclass
fields = dataclasses.fields(self.dc_type)
@ -171,7 +169,7 @@ class DataclassStruct(Adapter[t.Any, t.Any, DataclassType, DataclassType]):
fields = tuple(reversed(fields))
# extract the construct formats from the struct_type
subcon_fields = {}
subcon_fields: dict[str, t.Any] = {}
for field in fields:
subcon_fields[field.name] = field.metadata["subcon"]
@ -181,6 +179,7 @@ class DataclassStruct(Adapter[t.Any, t.Any, DataclassType, DataclassType]):
def __getattr__(self, name: str) -> t.Any:
return getattr(self.subcon, name)
@override
def _decode(
self, obj: "cs.Container[t.Any]", context: Context, path: PathType
) -> DataclassType:
@ -205,9 +204,10 @@ class DataclassStruct(Adapter[t.Any, t.Any, DataclassType, DataclassType]):
return dc # type: ignore
@override
def _encode(
self, obj: DataclassType, context: Context, path: PathType
) -> t.Dict[str, t.Any]:
) -> dict[str, t.Any]:
if not isinstance(obj, self.dc_type):
raise TypeError(f"'{repr(obj)}' has to be of type {repr(self.dc_type)}")
@ -215,20 +215,16 @@ class DataclassStruct(Adapter[t.Any, t.Any, DataclassType, DataclassType]):
fields = dataclasses.fields(self.dc_type)
# extract all fields from the container, that are used for create the dataclass object
ret_dict: t.Dict[str, t.Any] = {}
ret_dict: dict[str, t.Any] = {}
for field in fields:
value = getattr(obj, field.name)
ret_dict[field.name] = value
return ret_dict
def DataclassBitStruct(
dc_type: t.Type[DataclassType], reverse: bool = False
) -> t.Union[
"cs.Transformed[DataclassType, DataclassType]",
"cs.Restreamed[DataclassType, DataclassType]",
]:
dc_type: type[DataclassType], reverse: bool = False
) -> "cs.Transformed[DataclassType, DataclassType] | cs.Restreamed[DataclassType, DataclassType]":
r"""
Makes a DataclassStruct inside a Bitwise.
@ -255,6 +251,29 @@ def DataclassBitStruct(
"""
return cs.Bitwise(DataclassStruct(dc_type, reverse))
class EnhancedDataclassMixin(DataclassMixin):
@classmethod
def format(cls):
return DataclassStruct(cls)
@classmethod
def build(cls, obj: t.Self, **kw: dict[str, t.Any]):
return cls.format().build(obj, **kw)
@classmethod
def parse(cls, data: bytes | bytearray, **kw: dict[str, t.Any]):
return cls.format().parse(data, **kw)
@classmethod
def parse_file(cls, file: str, **kw: dict[str, t.Any]):
return cls.format().parse_file(file, **kw)
@classmethod
def parse_stream(cls, stream: t.IO[bytes], **kw: dict[str, t.Any]):
return cls.format().parse_stream(stream, **kw)
def build_self(self) -> bytes:
return self.build(self)
# support legacy names
TStruct = DataclassStruct

View file

@ -12,12 +12,14 @@ if t.TYPE_CHECKING:
# while type checking, the original classes are already generics, because they are defined like this in the stubs.
from construct import Adapter as Adapter
from construct import ConstantOrContextLambda as ConstantOrContextLambda
from construct import ConstantOrContextLambda2 as ConstantOrContextLambda2
from construct import Construct as Construct
from construct import Context as Context
from construct import ListContainer as ListContainer
from construct import PathType as PathType
from construct import Array as Array
from construct import Subconstruct as Subconstruct
from construct import Computed as Computed
else:
import construct as cs
@ -44,5 +46,12 @@ else:
):
pass
class Subconstruct(t.Generic[SubconParsedType, SubconBuildTypes, ParsedType, BuildTypes], cs.Subconstruct):
pass
class Computed(t.Generic[ParsedType], cs.Computed):
pass
ConstantOrContextLambda = t.Union[ValueType, t.Callable[[Context], t.Any]]
ConstantOrContextLambda2 = t.Union[ValueType, t.Callable[[Context], ValueType]]
PathType = str

View file

@ -1,9 +1,10 @@
# pyright: reportAny=false
import enum
import typing as t
from typing_extensions import Self
from typing_extensions import Self, override
from .generic_wrapper import *
from .generic_wrapper import Construct, Adapter, Context, PathType
# ## TEnum ############################################################################################################
@ -12,8 +13,8 @@ class EnumValue:
This is a helper class for adding documentation to an enum value.
"""
def __init__(self, value: int, doc: t.Optional[str] = None) -> None:
self.value = value
def __init__(self, value: int, doc: str | None = None) -> None:
self.value: int = value
self.__doc__ = doc if doc else ""
@ -47,7 +48,7 @@ class EnumBase(enum.IntEnum):
'This is the running state.'
"""
def __new__(cls, val: t.Union[EnumValue, int]) -> "Self":
def __new__(cls, val: EnumValue | int) -> "Self":
if isinstance(val, EnumValue):
obj = int.__new__(cls, val.value)
obj._value_ = val.value
@ -62,7 +63,8 @@ class EnumBase(enum.IntEnum):
# not found in the enum, a new pseudo member is created.
# The idea is taken from: https://stackoverflow.com/a/57179436
@classmethod
def _missing_(cls, value: t.Any) -> t.Optional[enum.Enum]:
@override
def _missing_(cls, value: t.Any) -> enum.Enum | None:
if isinstance(value, int):
pseudo_member = cls._value2member_map_.get(value, None)
if pseudo_member is None:
@ -76,7 +78,8 @@ class EnumBase(enum.IntEnum):
return pseudo_member
return None # will raise the ValueError in Enum.__new__
def __reduce_ex__(self, proto: t.Any) -> t.Tuple[t.Any, ...]:
@override
def __reduce_ex__(self, proto: t.Any) -> tuple[t.Any, ...]:
"""
Pickle enums by value instead of name (restores pre-3.11 behavior).
See https://github.com/python/cpython/pull/26658 for why this exists.
@ -91,21 +94,18 @@ class TEnum(Adapter[int, int, EnumType, EnumType]):
"""
Typed enum.
"""
def __init__(self, subcon: Construct[int, int], enum_type: t.Type[EnumType]):
if not issubclass(enum_type, EnumBase):
raise TypeError(
"'{}' has to be a '{}'".format(repr(enum_type), repr(EnumBase))
)
def __init__(self, subcon: Construct[int, int], enum_type: type[EnumType]):
# save enum type
self.enum_type = t.cast(t.Type[EnumType], enum_type) # type: ignore
self.enum_type: type[EnumType] = enum_type
# init adatper
super(TEnum, self).__init__(subcon) # type: ignore
@override
def _decode(self, obj: int, context: Context, path: PathType) -> EnumType:
return self.enum_type(obj)
@override
def _encode(
self,
obj: EnumType,
@ -152,7 +152,7 @@ class FlagsEnumBase(enum.IntFlag):
'This is option two.'
"""
def __new__(cls, val: t.Union[EnumValue, int]) -> "Self":
def __new__(cls, val: EnumValue | int) -> "Self":
if isinstance(val, EnumValue):
obj = int.__new__(cls, val.value)
obj._value_ = val.value
@ -164,6 +164,7 @@ class FlagsEnumBase(enum.IntFlag):
return obj
@classmethod
@override
def _missing_(cls, value: t.Any) -> t.Any:
"""
Returns member (possibly creating it) if one can be found for value.
@ -172,7 +173,8 @@ class FlagsEnumBase(enum.IntFlag):
new_member.__doc__ = "missing value"
return new_member
def __reduce_ex__(self, proto: t.Any) -> t.Tuple[t.Any, ...]:
@override
def __reduce_ex__(self, proto: t.Any) -> tuple[t.Any, ...]:
"""
Pickle enums by value instead of name (restores pre-3.11 behavior).
See https://github.com/python/cpython/pull/26658 for why this exists.
@ -187,21 +189,18 @@ class TFlagsEnum(Adapter[int, int, FlagsEnumType, FlagsEnumType]):
"""
Typed enum.
"""
def __init__(self, subcon: Construct[int, int], enum_type: t.Type[FlagsEnumType]):
if not issubclass(enum_type, FlagsEnumBase):
raise TypeError(
"'{}' has to be a '{}'".format(repr(enum_type), repr(FlagsEnumBase))
)
def __init__(self, subcon: Construct[int, int], enum_type: type[FlagsEnumType]):
# save enum type
self.enum_type = t.cast(t.Type[FlagsEnumType], enum_type) # type: ignore
self.enum_type: type[FlagsEnumType] = enum_type
# init adatper
super(TFlagsEnum, self).__init__(subcon) # type: ignore
@override
def _decode(self, obj: int, context: Context, path: PathType) -> FlagsEnumType:
return self.enum_type(obj)
@override
def _encode(
self,
obj: FlagsEnumType,

View file

@ -1,2 +1,2 @@
version = (0, 7, 0)
version_string = "0.7.0"
version_string = "0.7.0+wrapper"