Compare commits
25 commits
main
...
feature/py
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e730fd8d87 | ||
|
|
af1a7799e9 | ||
|
|
c5d08f9b87 | ||
|
|
acc9f6a9e4 | ||
|
|
935f021a4f | ||
|
|
a46c180e7a | ||
|
|
cab9a07c4f | ||
|
|
8529bd2878 | ||
|
|
46fc3dd48c | ||
|
|
f9e2d24d71 | ||
|
|
a0cadcce85 | ||
|
|
b21c43ca65 | ||
|
|
75dbd8d822 | ||
|
|
be5ae240fb | ||
|
|
94a8097bec | ||
|
|
e99dd5d752 | ||
|
|
8525c04165 | ||
|
|
7c68aeecd4 | ||
|
|
96ef565044 | ||
|
|
de76415ac5 | ||
|
|
9a9b4ab95a | ||
|
|
8169f0ed31 | ||
|
|
b896457f90 | ||
|
|
753e4282ee | ||
|
|
db9b35a0c2 |
10 changed files with 2115 additions and 448 deletions
6
.vscode/launch.json
vendored
6
.vscode/launch.json
vendored
|
|
@ -9,12 +9,6 @@
|
||||||
"type": "python",
|
"type": "python",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"program": "${file}",
|
"program": "${file}",
|
||||||
"console": "integratedTerminal"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Debug Tests",
|
|
||||||
"type": "python",
|
|
||||||
"request": "test",
|
|
||||||
"console": "integratedTerminal",
|
"console": "integratedTerminal",
|
||||||
"justMyCode": false
|
"justMyCode": false
|
||||||
}
|
}
|
||||||
|
|
|
||||||
21
.vscode/settings.json
vendored
21
.vscode/settings.json
vendored
|
|
@ -1,24 +1,25 @@
|
||||||
{
|
{
|
||||||
"python.pythonPath": "python",
|
|
||||||
"python.languageServer": "Pylance",
|
"python.languageServer": "Pylance",
|
||||||
// "python.testing.unittestEnabled": false,
|
|
||||||
// "python.testing.nosetestsEnabled": false,
|
// configure code formating
|
||||||
// "python.testing.pytestEnabled": true,
|
|
||||||
"pythonTestExplorer.testFramework": "pytest",
|
|
||||||
"python.formatting.provider": "black",
|
"python.formatting.provider": "black",
|
||||||
"python.sortImports.path": "isort",
|
"python.sortImports.path": "isort",
|
||||||
"python.sortImports.args": [
|
"python.sortImports.args": [
|
||||||
"--profile=black",
|
"--profile=black",
|
||||||
],
|
],
|
||||||
// "[python]": {
|
|
||||||
// "editor.codeActionsOnSave": {
|
// configure pylance
|
||||||
// "source.organizeImports": true
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
"python.analysis.typeCheckingMode": "strict",
|
"python.analysis.typeCheckingMode": "strict",
|
||||||
"python.analysis.autoImportCompletions": false,
|
"python.analysis.autoImportCompletions": false,
|
||||||
"python.analysis.diagnosticSeverityOverrides": {
|
"python.analysis.diagnosticSeverityOverrides": {
|
||||||
"reportPrivateUsage": "information",
|
"reportPrivateUsage": "information",
|
||||||
"reportUntypedNamedTuple": "information",
|
"reportUntypedNamedTuple": "information",
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// configure pytest
|
||||||
|
"python.testing.pytestArgs": [
|
||||||
|
"tests"
|
||||||
|
],
|
||||||
|
"python.testing.unittestEnabled": false,
|
||||||
|
"python.testing.pytestEnabled": true,
|
||||||
}
|
}
|
||||||
|
|
@ -1,16 +1,10 @@
|
||||||
from .dataclass_struct import (
|
from construct_typed.generic import constr
|
||||||
|
from construct_typed.dataclass_struct import (
|
||||||
DataclassBitStruct,
|
DataclassBitStruct,
|
||||||
DataclassMixin,
|
|
||||||
DataclassStruct,
|
DataclassStruct,
|
||||||
TBitStruct,
|
csfield
|
||||||
TContainerBase,
|
|
||||||
TContainerMixin,
|
|
||||||
TStruct,
|
|
||||||
TStructField,
|
|
||||||
csfield,
|
|
||||||
sfield,
|
|
||||||
)
|
)
|
||||||
from .generic_wrapper import (
|
from construct_typed.generic import (
|
||||||
Adapter,
|
Adapter,
|
||||||
ConstantOrContextLambda,
|
ConstantOrContextLambda,
|
||||||
Construct,
|
Construct,
|
||||||
|
|
@ -18,23 +12,17 @@ from .generic_wrapper import (
|
||||||
ListContainer,
|
ListContainer,
|
||||||
PathType,
|
PathType,
|
||||||
)
|
)
|
||||||
from .tenum import EnumBase, FlagsEnumBase, TEnum, TFlagsEnum
|
from construct_typed.tenum import TEnum, TFlags, TEnumConstruct, TFlagsConstruct
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"DataclassBitStruct",
|
"DataclassBitStruct",
|
||||||
"DataclassMixin",
|
|
||||||
"DataclassStruct",
|
"DataclassStruct",
|
||||||
"TBitStruct",
|
"constr",
|
||||||
"TContainerBase",
|
|
||||||
"TContainerMixin",
|
|
||||||
"TStruct",
|
|
||||||
"TStructField",
|
|
||||||
"csfield",
|
"csfield",
|
||||||
"sfield",
|
|
||||||
"EnumBase",
|
|
||||||
"FlagsEnumBase",
|
|
||||||
"TEnum",
|
"TEnum",
|
||||||
"TFlagsEnum",
|
"TEnumConstruct",
|
||||||
|
"TFlags",
|
||||||
|
"TFlagsConstruct",
|
||||||
"Adapter",
|
"Adapter",
|
||||||
"ConstantOrContextLambda",
|
"ConstantOrContextLambda",
|
||||||
"Construct",
|
"Construct",
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,10 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# pyright: strict
|
# pyright: strict
|
||||||
import dataclasses
|
import dataclasses
|
||||||
|
import sys
|
||||||
import textwrap
|
import textwrap
|
||||||
import typing as t
|
import typing as t
|
||||||
|
import enum
|
||||||
|
|
||||||
import construct as cs
|
import construct as cs
|
||||||
from construct.lib.containers import (
|
from construct.lib.containers import (
|
||||||
|
|
@ -12,21 +14,290 @@ from construct.lib.containers import (
|
||||||
)
|
)
|
||||||
from construct.lib.py3compat import bytestringtype, reprstring, unicodestringtype
|
from construct.lib.py3compat import bytestringtype, reprstring, unicodestringtype
|
||||||
|
|
||||||
from .generic_wrapper import Adapter, Construct, Context, ParsedType, PathType
|
from construct_typed.generic import Adapter, Construct, Context, ParsedType, PathType
|
||||||
|
|
||||||
|
# The `key_only` keyword is possible since python 3.10. To support it in python 3.8 & 3.9
|
||||||
|
# the dataclasses module from python 3.10 is copied to this package.
|
||||||
|
if sys.version_info >= (3, 10) or t.TYPE_CHECKING:
|
||||||
|
import dataclasses
|
||||||
|
else:
|
||||||
|
import construct_typed.dataclasses_py310 as dataclasses
|
||||||
|
|
||||||
|
T = t.TypeVar("T")
|
||||||
|
|
||||||
|
|
||||||
class DataclassMixin:
|
# Static type inference support via __dataclass_transform__ implemented as per:
|
||||||
|
# https://github.com/microsoft/pyright/blob/1.1.135/specs/dataclass_transforms.md
|
||||||
|
def __dataclass_transform__(
|
||||||
|
*,
|
||||||
|
eq_default: bool = True,
|
||||||
|
order_default: bool = False,
|
||||||
|
kw_only_default: bool = False,
|
||||||
|
field_descriptors: t.Tuple[t.Union[type, t.Callable[..., t.Any]], ...] = (()),
|
||||||
|
) -> t.Callable[[T], T]:
|
||||||
|
return lambda a: a
|
||||||
|
|
||||||
|
|
||||||
|
# this is nessesary, because typing.Literal type has to be an enum
|
||||||
|
class Flag(enum.Enum):
|
||||||
|
MISSING = dataclasses.MISSING
|
||||||
|
|
||||||
|
|
||||||
|
DATACLASS_METADATA_KEY = "__construct_typed_subcon"
|
||||||
|
|
||||||
|
# specialisation for constructs, that builds from none -> this field does not appear in the __init__ method and has a default of None
|
||||||
|
@t.overload
|
||||||
|
def csfield( # type: ignore
|
||||||
|
subcon: "Construct[ParsedType, None]",
|
||||||
|
*,
|
||||||
|
doc: t.Optional[str] = ...,
|
||||||
|
init: t.Literal[False] = ...,
|
||||||
|
default: t.Literal[None] = ...,
|
||||||
|
const: t.Literal[Flag.MISSING] = ...,
|
||||||
|
) -> t.Optional[ParsedType]:
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
|
# normal mode, when neither default nor const is defined -> this field does appear in the __init__ method but has no default value
|
||||||
|
@t.overload
|
||||||
|
def csfield(
|
||||||
|
subcon: "Construct[ParsedType, t.Any]",
|
||||||
|
*,
|
||||||
|
doc: t.Optional[str] = ...,
|
||||||
|
init: t.Literal[True] = ...,
|
||||||
|
default: t.Literal[Flag.MISSING] = ...,
|
||||||
|
const: t.Literal[Flag.MISSING] = ...,
|
||||||
|
) -> ParsedType:
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
|
# specialisation when const parameter is set -> this field does not appear in the __init__ method but has a default value
|
||||||
|
@t.overload
|
||||||
|
def csfield(
|
||||||
|
subcon: "Construct[ParsedType, t.Any]",
|
||||||
|
*,
|
||||||
|
doc: t.Optional[str] = ...,
|
||||||
|
init: t.Literal[False] = ...,
|
||||||
|
default: t.Literal[Flag.MISSING] = ...,
|
||||||
|
const: t.Optional[ParsedType] = ...,
|
||||||
|
) -> ParsedType:
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
|
# specialisation when default parameter is set -> this field does appear in the __init__ method and has a default value
|
||||||
|
@t.overload
|
||||||
|
def csfield(
|
||||||
|
subcon: "Construct[ParsedType, t.Any]",
|
||||||
|
*,
|
||||||
|
doc: t.Optional[str] = ...,
|
||||||
|
init: t.Literal[True] = ...,
|
||||||
|
default: t.Optional[ParsedType] = ...,
|
||||||
|
const: t.Literal[Flag.MISSING] = ...,
|
||||||
|
) -> ParsedType:
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
|
def csfield(
|
||||||
|
subcon: "Construct[ParsedType, t.Any]",
|
||||||
|
*,
|
||||||
|
doc: t.Optional[str] = None,
|
||||||
|
init: bool = True, # dont use `init`, this is only used for `dataclass_transform`
|
||||||
|
default: t.Optional[t.Any] = Flag.MISSING,
|
||||||
|
const: t.Optional[t.Any] = Flag.MISSING,
|
||||||
|
) -> t.Optional[ParsedType]:
|
||||||
"""
|
"""
|
||||||
Mixin for the dataclasses which are passed to "DataclassStruct" and "DataclassBitStruct".
|
Helper method for "DataclassStruct" and "DataclassBitStruct" to create the dataclass fields.
|
||||||
|
|
||||||
Note: This implementation is different to the 'cs.Container' of the original 'construct'
|
This method also processes Const and Default, to pass these values als default values to the dataclass.
|
||||||
library. In the original 'cs.Container' some names like "update", "keys", "items", ... can
|
|
||||||
only accessed via key access (square brackets) and not via attribute access (dot operator),
|
Only one of the parameters `default` or `const` can be vaild. They are mutually exclusive.
|
||||||
because they are also method names. This implementation is based on "dataclasses.dataclass"
|
|
||||||
which only uses modul-level instead of instance-level helper methods.So no instance-level
|
|
||||||
methods exists and every name can be used.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
if (default is not Flag.MISSING) and (const is not Flag.MISSING):
|
||||||
|
raise ValueError("default and const are mutally exclusive")
|
||||||
|
|
||||||
|
# Rename subcon, if doc is available
|
||||||
|
if doc is not None:
|
||||||
|
doc = textwrap.dedent(doc).strip("\n")
|
||||||
|
subcon = cs.Renamed(subcon, newdocs=doc)
|
||||||
|
|
||||||
|
if default is not Flag.MISSING:
|
||||||
|
init = True
|
||||||
|
default = default
|
||||||
|
subcon = cs.Default(subcon, default)
|
||||||
|
elif const is not Flag.MISSING:
|
||||||
|
init = False
|
||||||
|
default = const
|
||||||
|
subcon = cs.Const(const, subcon)
|
||||||
|
elif subcon.flagbuildnone is True:
|
||||||
|
init = False
|
||||||
|
default = None
|
||||||
|
else:
|
||||||
|
init = True
|
||||||
|
default = dataclasses.MISSING
|
||||||
|
|
||||||
|
return t.cast(
|
||||||
|
t.Optional[ParsedType],
|
||||||
|
dataclasses.field(
|
||||||
|
default=default,
|
||||||
|
init=init,
|
||||||
|
metadata={DATACLASS_METADATA_KEY: subcon},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class DataclassConstruct(Adapter[t.Any, t.Any, T, T]):
|
||||||
|
r"""
|
||||||
|
TODO: Add Documentation
|
||||||
|
"""
|
||||||
|
subcon: "cs.Struct[t.Any, t.Any]"
|
||||||
|
if t.TYPE_CHECKING:
|
||||||
|
|
||||||
|
def __new__(
|
||||||
|
cls,
|
||||||
|
dc_type: t.Type[T],
|
||||||
|
reverse: bool = False,
|
||||||
|
) -> "DataclassConstruct[T]":
|
||||||
|
...
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
dc_type: t.Type[T],
|
||||||
|
reverse: bool = False,
|
||||||
|
) -> None:
|
||||||
|
if not issubclass(dc_type, DataclassStruct):
|
||||||
|
raise TypeError(
|
||||||
|
f"'{repr(dc_type)}' has to be a subclass of 'DataclassStruct'"
|
||||||
|
)
|
||||||
|
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
|
||||||
|
|
||||||
|
# get all fields from the dataclass
|
||||||
|
fields = dataclasses.fields(self.dc_type)
|
||||||
|
if self.reverse:
|
||||||
|
fields = tuple(reversed(fields))
|
||||||
|
|
||||||
|
# extract the construct formats from the struct_type
|
||||||
|
subcon_fields = {}
|
||||||
|
for field in fields:
|
||||||
|
subcon_fields[field.name] = field.metadata[DATACLASS_METADATA_KEY]
|
||||||
|
|
||||||
|
# init adatper
|
||||||
|
super().__init__(cs.Struct(**subcon_fields)) # type: ignore
|
||||||
|
|
||||||
|
def __getattr__(self, name: str) -> t.Any:
|
||||||
|
return getattr(self.subcon, name)
|
||||||
|
|
||||||
|
def _decode(
|
||||||
|
self, obj: "cs.Container[t.Any]", context: Context, path: PathType
|
||||||
|
) -> T:
|
||||||
|
# get all fields from the dataclass
|
||||||
|
fields = dataclasses.fields(self.dc_type)
|
||||||
|
|
||||||
|
# extract all fields from the container, that are used for create the dataclass object
|
||||||
|
dc_init = {}
|
||||||
|
for field in fields:
|
||||||
|
if field.init:
|
||||||
|
value = obj[field.name]
|
||||||
|
dc_init[field.name] = value
|
||||||
|
|
||||||
|
# create object of dataclass
|
||||||
|
dc: T = self.dc_type(**dc_init) # type: ignore
|
||||||
|
|
||||||
|
# extract all other values from the container, an pass it to the dataclass
|
||||||
|
for field in fields:
|
||||||
|
if not field.init:
|
||||||
|
value = obj[field.name]
|
||||||
|
setattr(dc, field.name, value)
|
||||||
|
|
||||||
|
return dc
|
||||||
|
|
||||||
|
def _encode(self, obj: T, context: Context, path: PathType) -> t.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)}")
|
||||||
|
|
||||||
|
# get all fields from the dataclass
|
||||||
|
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] = {}
|
||||||
|
for field in fields:
|
||||||
|
value = getattr(obj, field.name)
|
||||||
|
ret_dict[field.name] = value
|
||||||
|
|
||||||
|
return ret_dict
|
||||||
|
|
||||||
|
|
||||||
|
@__dataclass_transform__(field_descriptors=(csfield,), kw_only_default=True)
|
||||||
|
class DataclassStruct:
|
||||||
|
"""
|
||||||
|
Adapter for a dataclasses for optimised type hints / static autocompletion in comparision to the original Struct.
|
||||||
|
|
||||||
|
|
||||||
|
Before this construct can be created a dataclasses.dataclass type must be created, which must also derive from DataclassMixin. In this dataclass all fields must be assigned to a construct type using csfield.
|
||||||
|
|
||||||
|
Internally, all fields are converted to a normal Struct, which does the actual parsing/building.
|
||||||
|
|
||||||
|
Parses to a dataclasses.dataclass instance, and builds from such instance. Size is the sum of all subcon sizes, unless any subcon raises SizeofError.
|
||||||
|
|
||||||
|
Every construct that builds from None (eg. Const, Default, Index, Rebuild, Check, Checksum, ...) will automatically initialised with None.
|
||||||
|
If a default or const value should be used in a DataclassStruct the best is to use the `default` or `const` parameters of `csfield`. These
|
||||||
|
are internally used for creating a `Const` or `Default` construct but also adds the default/const value to the DataclassStruct while creating
|
||||||
|
it via __init__.
|
||||||
|
|
||||||
|
:param constr: Lambda for creating the construct object. This can be used if the DataclassStruct is nested inside a Subconstruct. Eg. `lambda cls: cs.Bitwise(cls)`.
|
||||||
|
:param reverse: Flag if the fields of the dataclass should be reversed
|
||||||
|
|
||||||
|
Example::
|
||||||
|
|
||||||
|
>>> from construct import Bytes, Int8ub, this
|
||||||
|
>>> from construct_typed import DataclassMixin, DataclassStruct, csfield, construct
|
||||||
|
... class Image(DataclassStruct):
|
||||||
|
... width: int = csfield(Int8ub)
|
||||||
|
... height: int = csfield(Int8ub)
|
||||||
|
... pixels: bytes = csfield(Bytes(this.height * this.width))
|
||||||
|
>>> d = construct(Image)
|
||||||
|
>>> d.parse(b"\x01\x0212")
|
||||||
|
Image(width=1, height=2, pixels=b'12')
|
||||||
|
"""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def __init_subclass__(
|
||||||
|
cls: t.Type[T],
|
||||||
|
constr: t.Callable[
|
||||||
|
[DataclassConstruct[T]], Construct[t.Any, t.Any]
|
||||||
|
] = lambda cls: cls,
|
||||||
|
reverse_fields: bool = False,
|
||||||
|
) -> None:
|
||||||
|
# validate types
|
||||||
|
if not callable(constr):
|
||||||
|
raise ValueError("`constr` parameter has to be a function or lambda")
|
||||||
|
if not isinstance(reverse_fields, bool): # type: ignore
|
||||||
|
raise ValueError("`reverse_fields` parameter has to be an `bool` object")
|
||||||
|
|
||||||
|
# get documentation before creating the dataclass
|
||||||
|
docs = ""
|
||||||
|
if cls.__doc__ is not None:
|
||||||
|
docs = textwrap.dedent(cls.__doc__).strip("\n")
|
||||||
|
|
||||||
|
# create dataclass
|
||||||
|
dataclasses.dataclass(cls, kw_only=True) # type: ignore
|
||||||
|
|
||||||
|
# create construct format
|
||||||
|
dc_constr = constr(DataclassConstruct(cls, reverse_fields))
|
||||||
|
if not isinstance(dc_constr, cs.Construct): # type: ignore
|
||||||
|
raise ValueError("`constr` sould return a `Construct` object")
|
||||||
|
|
||||||
|
# save docs
|
||||||
|
dc_constr.docs = docs
|
||||||
|
|
||||||
|
# save construct format and make the class compatible to `Constructable` protocol
|
||||||
|
setattr(cls, "__constr__", lambda: dc_constr)
|
||||||
|
|
||||||
|
# the `construct` library is using the [] access internally, so struct objects
|
||||||
|
# should also make this possible and not only via the dot access.
|
||||||
def __getitem__(self, key: str) -> t.Any:
|
def __getitem__(self, key: str) -> t.Any:
|
||||||
return getattr(self, key)
|
return getattr(self, key)
|
||||||
|
|
||||||
|
|
@ -72,201 +343,43 @@ class DataclassMixin:
|
||||||
text.append(indentation.join(str(v).split("\n")))
|
text.append(indentation.join(str(v).split("\n")))
|
||||||
return "".join(text)
|
return "".join(text)
|
||||||
|
|
||||||
|
|
||||||
def csfield(
|
|
||||||
subcon: Construct[ParsedType, t.Any],
|
|
||||||
doc: t.Optional[str] = None,
|
|
||||||
parsed: t.Optional[t.Callable[[t.Any, Context], None]] = None,
|
|
||||||
) -> ParsedType:
|
|
||||||
"""
|
|
||||||
Helper method for "DataclassStruct" and "DataclassBitStruct" to create the dataclass fields.
|
|
||||||
|
|
||||||
This method also processes Const and Default, to pass these values als default values to the dataclass.
|
|
||||||
"""
|
|
||||||
orig_subcon = subcon
|
|
||||||
|
|
||||||
# Rename subcon, if doc or parsed are available
|
|
||||||
if (doc is not None) or (parsed is not None):
|
|
||||||
if doc is not None:
|
|
||||||
doc = textwrap.dedent(doc).strip("\n")
|
|
||||||
subcon = cs.Renamed(subcon, newdocs=doc, newparsed=parsed)
|
|
||||||
|
|
||||||
if orig_subcon.flagbuildnone is True:
|
|
||||||
init = False
|
|
||||||
default = None
|
|
||||||
else:
|
|
||||||
init = True
|
|
||||||
default = dataclasses.MISSING
|
|
||||||
|
|
||||||
# Set default values in case of special sucons
|
|
||||||
if isinstance(orig_subcon, cs.Const):
|
|
||||||
const_subcon: "cs.Const[t.Any, t.Any, t.Any, t.Any]" = orig_subcon
|
|
||||||
default = const_subcon.value
|
|
||||||
elif isinstance(orig_subcon, cs.Default):
|
|
||||||
default_subcon: "cs.Default[t.Any, t.Any, t.Any, t.Any]" = orig_subcon
|
|
||||||
if callable(default_subcon.value):
|
|
||||||
default = None # context lambda is only defined at parsing/building
|
|
||||||
else:
|
|
||||||
default = default_subcon.value
|
|
||||||
|
|
||||||
return t.cast(
|
|
||||||
ParsedType,
|
|
||||||
dataclasses.field(
|
|
||||||
default=default,
|
|
||||||
init=init,
|
|
||||||
metadata={"subcon": subcon},
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
DataclassType = t.TypeVar("DataclassType", bound=DataclassMixin)
|
|
||||||
|
|
||||||
|
|
||||||
class DataclassStruct(Adapter[t.Any, t.Any, DataclassType, DataclassType]):
|
|
||||||
"""
|
|
||||||
Adapter for a dataclasses for optimised type hints / static autocompletion in comparision to the original Struct.
|
|
||||||
|
|
||||||
Before this construct can be created a dataclasses.dataclass type must be created, which must also derive from DataclassMixin. In this dataclass all fields must be assigned to a construct type using csfield.
|
|
||||||
|
|
||||||
Internally, all fields are converted to a Struct, which does the actual parsing/building.
|
|
||||||
|
|
||||||
Parses to a dataclasses.dataclass instance, and builds from such instance. Size is the sum of all subcon sizes, unless any subcon raises SizeofError.
|
|
||||||
|
|
||||||
:param dc_type: Type of the dataclass, which also inherits from DataclassMixin
|
|
||||||
:param reverse: Flag if the fields of the dataclass should be reversed
|
|
||||||
|
|
||||||
Example::
|
|
||||||
|
|
||||||
>>> import dataclasses
|
|
||||||
>>> from construct import Bytes, Int8ub, this
|
|
||||||
>>> from construct_typed import DataclassMixin, DataclassStruct, csfield
|
|
||||||
>>> @dataclasses.dataclass
|
|
||||||
... class Image(DataclassMixin):
|
|
||||||
... width: int = csfield(Int8ub)
|
|
||||||
... height: int = csfield(Int8ub)
|
|
||||||
... pixels: bytes = csfield(Bytes(this.height * this.width))
|
|
||||||
>>> d = DataclassStruct(Image)
|
|
||||||
>>> d.parse(b"\x01\x0212")
|
|
||||||
Image(width=1, height=2, pixels=b'12')
|
|
||||||
"""
|
|
||||||
|
|
||||||
subcon: "cs.Struct[t.Any, t.Any]"
|
|
||||||
if t.TYPE_CHECKING:
|
if t.TYPE_CHECKING:
|
||||||
|
|
||||||
def __new__(
|
@classmethod
|
||||||
cls,
|
def __constr__(cls: t.Type[T]) -> "DataclassConstruct[T]":
|
||||||
dc_type: t.Type[DataclassType],
|
|
||||||
reverse: bool = False,
|
|
||||||
) -> "DataclassStruct[DataclassType]":
|
|
||||||
...
|
...
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
dc_type: t.Type[DataclassType],
|
|
||||||
reverse: bool = False,
|
|
||||||
) -> None:
|
|
||||||
if not issubclass(dc_type, DataclassMixin):
|
|
||||||
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
|
|
||||||
|
|
||||||
# get all fields from the dataclass
|
class DataclassBitStruct(DataclassStruct):
|
||||||
fields = dataclasses.fields(self.dc_type)
|
|
||||||
if self.reverse:
|
|
||||||
fields = tuple(reversed(fields))
|
|
||||||
|
|
||||||
# extract the construct formats from the struct_type
|
|
||||||
subcon_fields = {}
|
|
||||||
for field in fields:
|
|
||||||
subcon_fields[field.name] = field.metadata["subcon"]
|
|
||||||
|
|
||||||
# init adatper
|
|
||||||
super().__init__(cs.Struct(**subcon_fields)) # type: ignore
|
|
||||||
|
|
||||||
def __getattr__(self, name: str) -> t.Any:
|
|
||||||
return getattr(self.subcon, name)
|
|
||||||
|
|
||||||
def _decode(
|
|
||||||
self, obj: "cs.Container[t.Any]", context: Context, path: PathType
|
|
||||||
) -> DataclassType:
|
|
||||||
# get all fields from the dataclass
|
|
||||||
fields = dataclasses.fields(self.dc_type)
|
|
||||||
|
|
||||||
# extract all fields from the container, that are used for create the dataclass object
|
|
||||||
dc_init = {}
|
|
||||||
for field in fields:
|
|
||||||
if field.init:
|
|
||||||
value = obj[field.name]
|
|
||||||
dc_init[field.name] = value
|
|
||||||
|
|
||||||
# create object of dataclass
|
|
||||||
dc = self.dc_type(**dc_init) # type: ignore
|
|
||||||
|
|
||||||
# extract all other values from the container, an pass it to the dataclass
|
|
||||||
for field in fields:
|
|
||||||
if not field.init:
|
|
||||||
value = obj[field.name]
|
|
||||||
setattr(dc, field.name, value)
|
|
||||||
|
|
||||||
return dc
|
|
||||||
|
|
||||||
def _encode(
|
|
||||||
self, obj: DataclassType, context: Context, path: PathType
|
|
||||||
) -> t.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)}")
|
|
||||||
|
|
||||||
# get all fields from the dataclass
|
|
||||||
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] = {}
|
|
||||||
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]",
|
|
||||||
]:
|
|
||||||
r"""
|
r"""
|
||||||
Makes a DataclassStruct inside a Bitwise.
|
Makes a DataclassStruct inside a Bitwise.
|
||||||
|
|
||||||
See :class:`~construct.core.Bitwise` and :class:`~construct_typed.dataclass_struct.DatclassStruct` for semantics and raisable exceptions.
|
See :class:`~construct.core.Bitwise` and :class:`~construct_typed.dataclass_struct.DatclassStruct` for semantics and raisable exceptions.
|
||||||
|
|
||||||
:param dc_type: Type of the dataclass, which also inherits from DataclassMixin
|
:param constr: TODO
|
||||||
:param reverse: Flag if the fields of the dataclass should be reversed
|
:param reverse_fields: Flag if the fields of the dataclass should be reversed
|
||||||
|
|
||||||
Example::
|
Example::
|
||||||
|
|
||||||
DataclassBitStruct <--> Bitwise(DataclassStruct(...))
|
TODO:
|
||||||
>>> import dataclasses
|
|
||||||
>>> from construct import BitsInteger, Flag, Nibble, Padding
|
>>> from construct import BitsInteger, Flag, Nibble, Padding
|
||||||
>>> from construct_typed import DataclassBitStruct, DataclassMixin, csfield
|
>>> from construct_typed import DataclassBitStruct, csfield, construct
|
||||||
>>> @dataclasses.dataclass
|
... class TestDataclass(DataclassBitStruct):
|
||||||
... class TestDataclass(DataclassMixin):
|
|
||||||
... a: int = csfield(Flag)
|
... a: int = csfield(Flag)
|
||||||
... b: int = csfield(Nibble)
|
... b: int = csfield(Nibble)
|
||||||
... c: int = csfield(BitsInteger(10))
|
... c: int = csfield(BitsInteger(10))
|
||||||
... d: None = csfield(Padding(1))
|
... d: None = csfield(Padding(1))
|
||||||
>>> d = DataclassBitStruct(TestDataclass)
|
>>> d = construct(TestDataclass)
|
||||||
>>> d.parse(b"\x01\x02")
|
>>> d.parse(b"\x01\x02")
|
||||||
TestDataclass(a=False, b=0, c=129, d=None)
|
TestDataclass(a=False, b=0, c=129, d=None)
|
||||||
"""
|
"""
|
||||||
return cs.Bitwise(DataclassStruct(dc_type, reverse))
|
|
||||||
|
|
||||||
|
@classmethod
|
||||||
# support legacy names
|
def __init_subclass__(
|
||||||
TStruct = DataclassStruct
|
cls: t.Type[T],
|
||||||
TBitStruct = DataclassBitStruct
|
constr: t.Callable[
|
||||||
TContainerMixin = DataclassMixin
|
[DataclassConstruct[T]], Construct[t.Any, t.Any]
|
||||||
TContainerBase = DataclassMixin
|
] = lambda cls: cls,
|
||||||
TStructField = csfield
|
reverse_fields: bool = False,
|
||||||
sfield = csfield
|
) -> None:
|
||||||
|
DataclassStruct.__init_subclass__.__func__(cls, lambda cls: cs.Bitwise(constr(cls)), reverse_fields) # type: ignore
|
||||||
|
|
|
||||||
1462
construct_typed/dataclasses_py310.py
Normal file
1462
construct_typed/dataclasses_py310.py
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -39,3 +39,20 @@ else:
|
||||||
|
|
||||||
ConstantOrContextLambda = t.Union[ValueType, t.Callable[[Context], t.Any]]
|
ConstantOrContextLambda = t.Union[ValueType, t.Callable[[Context], t.Any]]
|
||||||
PathType = str
|
PathType = str
|
||||||
|
|
||||||
|
|
||||||
|
@t.runtime_checkable
|
||||||
|
class Constructable(t.Protocol[ParsedType, BuildTypes]):
|
||||||
|
def __constr__(self) -> "Construct[ParsedType, BuildTypes]":
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
|
def constr(
|
||||||
|
constr: t.Union[
|
||||||
|
Constructable[ParsedType, BuildTypes], "Construct[ParsedType, BuildTypes]"
|
||||||
|
],
|
||||||
|
) -> Construct[ParsedType, BuildTypes]:
|
||||||
|
"""Get construct instance of `Constructable` or `Construct`"""
|
||||||
|
if isinstance(constr, Constructable):
|
||||||
|
constr = constr.__constr__()
|
||||||
|
return constr
|
||||||
|
|
@ -1,28 +1,106 @@
|
||||||
import enum
|
import enum
|
||||||
|
import textwrap
|
||||||
import typing as t
|
import typing as t
|
||||||
|
|
||||||
from .generic_wrapper import *
|
import construct as cs
|
||||||
|
|
||||||
|
from construct_typed.generic import *
|
||||||
|
|
||||||
|
T = t.TypeVar("T")
|
||||||
|
|
||||||
|
|
||||||
# ## TEnum ############################################################################################################
|
class _EnumMeta(enum.EnumMeta):
|
||||||
class EnumBase(enum.IntEnum):
|
@classmethod
|
||||||
|
def __prepare__(
|
||||||
|
metacls, # type: ignore
|
||||||
|
__name: str,
|
||||||
|
__bases: t.Tuple[type, ...],
|
||||||
|
**kwargs: t.Any,
|
||||||
|
) -> t.Mapping[str, object]:
|
||||||
|
# This method is needed, because the original __prepare__ method does not accept kwargs.
|
||||||
|
return super().__prepare__(__name, __bases)
|
||||||
|
|
||||||
|
def __new__(
|
||||||
|
metacls: t.Type[T], # type: ignore
|
||||||
|
__name: str,
|
||||||
|
__bases: t.Tuple[type, ...],
|
||||||
|
__namespace: t.Dict[str, t.Any],
|
||||||
|
**kwargs: t.Any,
|
||||||
|
) -> T:
|
||||||
|
# get documentation before creating the enum
|
||||||
|
docs = ""
|
||||||
|
if "__doc__" in __namespace:
|
||||||
|
docs = textwrap.dedent(__namespace["__doc__"]).strip("\n")
|
||||||
|
|
||||||
|
# create new enum object
|
||||||
|
cls: T = super().__new__(metacls, __name, __bases, __namespace) # type: ignore
|
||||||
|
|
||||||
|
# if the `TEnum` class is created, there are no parameters
|
||||||
|
if len(kwargs) == 0:
|
||||||
|
return cls
|
||||||
|
|
||||||
|
# extract parameters from kwargs
|
||||||
|
subcon: "cs.Construct[t.Any, t.Any]" = kwargs.pop("subcon", None)
|
||||||
|
if not isinstance(subcon, cs.Construct): # type: ignore
|
||||||
|
raise ValueError(
|
||||||
|
f"`subcon` parameter has to be an `Construct` object but is {type(subcon)}"
|
||||||
|
)
|
||||||
|
if len(kwargs) > 0: # check remaining parameters
|
||||||
|
unsupp_parm = ", ".join([f"'{k}'" for k in kwargs.keys()])
|
||||||
|
raise ValueError(f"unsupported parameter(s) detected: {unsupp_parm}")
|
||||||
|
|
||||||
|
# create construct format
|
||||||
|
if TEnum in __bases:
|
||||||
|
enum_constr = TEnumConstruct(subcon, cls) # type: ignore
|
||||||
|
elif TFlags in __bases:
|
||||||
|
enum_constr = TFlagsConstruct(subcon, cls) # type: ignore
|
||||||
|
else:
|
||||||
|
raise TypeError("neither `TEnum` nor `TFlags` in bases")
|
||||||
|
|
||||||
|
# save documentation
|
||||||
|
enum_constr.docs = docs
|
||||||
|
|
||||||
|
# save construct format and make the class compatible to `Constructable` protocol
|
||||||
|
setattr(cls, "__constr__", lambda: enum_constr) # type: ignore
|
||||||
|
|
||||||
|
return cls
|
||||||
|
|
||||||
|
|
||||||
|
# ## TEnumConstruct ############################################################################################################
|
||||||
|
class TEnum(enum.IntEnum, metaclass=_EnumMeta):
|
||||||
"""
|
"""
|
||||||
Base class for an Enum used in `construct_typed.TEnum`.
|
Base class for an Enum used in `construct_typed.TEnumConstruct`.
|
||||||
|
|
||||||
This class extends the standard `enum.IntEnum`, so that missing values are automatically generated.
|
This class extends the standard `enum.IntEnum`, so that missing values are automatically generated.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
if t.TYPE_CHECKING:
|
||||||
|
# unfortunately the metaclass `enum.EnumMeta` does not forward the parameters to __init_subclass__, so that
|
||||||
|
# we have to make our own metaclass `ConstructEnumMeta`.
|
||||||
|
# But pylance/pyright is checking the type parameters passed to the class via __init_subclass__, so that we
|
||||||
|
# have to fake one.
|
||||||
|
@classmethod
|
||||||
|
def __init_subclass__(
|
||||||
|
cls,
|
||||||
|
subcon: "cs.Construct[t.Any, t.Any]",
|
||||||
|
) -> None:
|
||||||
|
...
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def __constr__(cls: "t.Type[EnumType]") -> "TEnumConstruct[EnumType]":
|
||||||
|
...
|
||||||
|
|
||||||
# Extend the enum type with __missing__ method. So if a enum value
|
# Extend the enum type with __missing__ method. So if a enum value
|
||||||
# not found in the enum, a new pseudo member is created.
|
# not found in the enum, a new pseudo member is created.
|
||||||
# The idea is taken from: https://stackoverflow.com/a/57179436
|
# The idea is taken from: https://stackoverflow.com/a/57179436
|
||||||
@classmethod
|
@classmethod
|
||||||
def _missing_(cls, value: t.Any) -> t.Optional["EnumBase"]:
|
def _missing_(cls, value: t.Any) -> t.Optional["TEnum"]:
|
||||||
if isinstance(value, int):
|
if isinstance(value, int):
|
||||||
return cls._create_pseudo_member_(value)
|
return cls._create_pseudo_member_(value)
|
||||||
return None # will raise the ValueError in Enum.__new__
|
return None # will raise the ValueError in Enum.__new__
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _create_pseudo_member_(cls, value: int) -> "EnumBase":
|
def _create_pseudo_member_(cls, value: int) -> "TEnum":
|
||||||
pseudo_member = cls._value2member_map_.get(value, None) # type: ignore
|
pseudo_member = cls._value2member_map_.get(value, None) # type: ignore
|
||||||
if pseudo_member is None:
|
if pseudo_member is None:
|
||||||
new_member = int.__new__(cls, value)
|
new_member = int.__new__(cls, value)
|
||||||
|
|
@ -34,10 +112,10 @@ class EnumBase(enum.IntEnum):
|
||||||
return pseudo_member # type: ignore
|
return pseudo_member # type: ignore
|
||||||
|
|
||||||
|
|
||||||
EnumType = t.TypeVar("EnumType", bound=EnumBase)
|
EnumType = t.TypeVar("EnumType", bound=TEnum)
|
||||||
|
|
||||||
|
|
||||||
class TEnum(Adapter[int, int, EnumType, EnumType]):
|
class TEnumConstruct(Adapter[int, int, EnumType, EnumType]):
|
||||||
"""
|
"""
|
||||||
Typed enum.
|
Typed enum.
|
||||||
"""
|
"""
|
||||||
|
|
@ -46,20 +124,20 @@ class TEnum(Adapter[int, int, EnumType, EnumType]):
|
||||||
|
|
||||||
def __new__(
|
def __new__(
|
||||||
cls, subcon: Construct[int, int], enum_type: t.Type[EnumType]
|
cls, subcon: Construct[int, int], enum_type: t.Type[EnumType]
|
||||||
) -> "TEnum[EnumType]":
|
) -> "TEnumConstruct[EnumType]":
|
||||||
...
|
...
|
||||||
|
|
||||||
def __init__(self, subcon: Construct[int, int], enum_type: t.Type[EnumType]):
|
def __init__(self, subcon: Construct[int, int], enum_type: t.Type[EnumType]):
|
||||||
if not issubclass(enum_type, EnumBase):
|
if not issubclass(enum_type, TEnum):
|
||||||
raise TypeError(
|
raise TypeError(
|
||||||
"'{}' has to be a '{}'".format(repr(enum_type), repr(EnumBase))
|
"'{}' has to be a '{}'".format(repr(enum_type), repr(TEnum))
|
||||||
)
|
)
|
||||||
|
|
||||||
# save enum type
|
# save enum type
|
||||||
self.enum_type = t.cast(t.Type[EnumType], enum_type) # type: ignore
|
self.enum_type = t.cast(t.Type[EnumType], enum_type) # type: ignore
|
||||||
|
|
||||||
# init adatper
|
# init adatper
|
||||||
super(TEnum, self).__init__(subcon) # type: ignore
|
super(TEnumConstruct, self).__init__(subcon) # type: ignore
|
||||||
|
|
||||||
def _decode(self, obj: int, context: Context, path: PathType) -> EnumType:
|
def _decode(self, obj: int, context: Context, path: PathType) -> EnumType:
|
||||||
return self.enum_type(obj)
|
return self.enum_type(obj)
|
||||||
|
|
@ -77,44 +155,60 @@ class TEnum(Adapter[int, int, EnumType, EnumType]):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
# ## TFlagsEnum #######################################################################################################
|
# ## TFlags #######################################################################################################
|
||||||
class FlagsEnumBase(enum.IntFlag):
|
class TFlags(enum.IntFlag, metaclass=_EnumMeta):
|
||||||
pass
|
if t.TYPE_CHECKING:
|
||||||
|
# unfortunately the metaclass `enum.EnumMeta` does not forward the parameters to __init_subclass__, so that
|
||||||
|
# we have to make our own metaclass `ConstructEnumMeta`.
|
||||||
|
# But pylance/pyright is checking the type parameters passed to the class via __init_subclass__, so that we
|
||||||
|
# have to fake one.
|
||||||
|
@classmethod
|
||||||
|
def __init_subclass__(
|
||||||
|
cls,
|
||||||
|
subcon: "cs.Construct[t.Any, t.Any]",
|
||||||
|
) -> None:
|
||||||
|
...
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def __constr__(
|
||||||
|
cls: "t.Type[FlagsType]",
|
||||||
|
) -> "TFlagsConstruct[FlagsType]":
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
FlagsEnumType = t.TypeVar("FlagsEnumType", bound=FlagsEnumBase)
|
FlagsType = t.TypeVar("FlagsType", bound=TFlags)
|
||||||
|
|
||||||
|
|
||||||
class TFlagsEnum(Adapter[int, int, FlagsEnumType, FlagsEnumType]):
|
class TFlagsConstruct(Adapter[int, int, FlagsType, FlagsType]):
|
||||||
"""
|
"""
|
||||||
Typed enum.
|
Typed flags.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if t.TYPE_CHECKING:
|
if t.TYPE_CHECKING:
|
||||||
|
|
||||||
def __new__(
|
def __new__(
|
||||||
cls, subcon: Construct[int, int], enum_type: t.Type[FlagsEnumType]
|
cls, subcon: Construct[int, int], enum_type: t.Type[FlagsType]
|
||||||
) -> "TFlagsEnum[FlagsEnumType]":
|
) -> "TFlagsConstruct[FlagsType]":
|
||||||
...
|
...
|
||||||
|
|
||||||
def __init__(self, subcon: Construct[int, int], enum_type: t.Type[FlagsEnumType]):
|
def __init__(self, subcon: Construct[int, int], enum_type: t.Type[FlagsType]):
|
||||||
if not issubclass(enum_type, FlagsEnumBase):
|
if not issubclass(enum_type, TFlags):
|
||||||
raise TypeError(
|
raise TypeError(
|
||||||
"'{}' has to be a '{}'".format(repr(enum_type), repr(FlagsEnumBase))
|
"'{}' has to be a '{}'".format(repr(enum_type), repr(TFlags))
|
||||||
)
|
)
|
||||||
|
|
||||||
# save enum type
|
# save enum type
|
||||||
self.enum_type = t.cast(t.Type[FlagsEnumType], enum_type) # type: ignore
|
self.enum_type = t.cast(t.Type[FlagsType], enum_type) # type: ignore
|
||||||
|
|
||||||
# init adatper
|
# init adatper
|
||||||
super(TFlagsEnum, self).__init__(subcon) # type: ignore
|
super(TFlagsConstruct, self).__init__(subcon) # type: ignore
|
||||||
|
|
||||||
def _decode(self, obj: int, context: Context, path: PathType) -> FlagsEnumType:
|
def _decode(self, obj: int, context: Context, path: PathType) -> FlagsType:
|
||||||
return self.enum_type(obj)
|
return self.enum_type(obj)
|
||||||
|
|
||||||
def _encode(
|
def _encode(
|
||||||
self,
|
self,
|
||||||
obj: FlagsEnumType,
|
obj: FlagsType,
|
||||||
context: Context,
|
context: Context,
|
||||||
path: PathType,
|
path: PathType,
|
||||||
) -> int:
|
) -> int:
|
||||||
|
|
|
||||||
|
|
@ -1,2 +1,2 @@
|
||||||
version = (0, 5, 2)
|
version = (0, 5, 2)
|
||||||
version_string = "0.5.2"
|
version_string = "0.5.2"
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import construct_typed as cst
|
||||||
Buffer = t.Union[bytes, memoryview, bytearray]
|
Buffer = t.Union[bytes, memoryview, bytearray]
|
||||||
ParsedType = t.TypeVar("ParsedType")
|
ParsedType = t.TypeVar("ParsedType")
|
||||||
BuildTypes = t.TypeVar("BuildTypes")
|
BuildTypes = t.TypeVar("BuildTypes")
|
||||||
ContainerType = t.TypeVar("ContainerType", bound=cst.TContainerMixin)
|
ContainerType = t.TypeVar("ContainerType", bound=cst.DataclassStruct)
|
||||||
T = t.TypeVar("T")
|
T = t.TypeVar("T")
|
||||||
|
|
||||||
IdentType = t.TypeVar("IdentType")
|
IdentType = t.TypeVar("IdentType")
|
||||||
|
|
@ -20,7 +20,7 @@ def raises(
|
||||||
) -> t.Union[t.Any, Exception]: ...
|
) -> t.Union[t.Any, Exception]: ...
|
||||||
@t.overload
|
@t.overload
|
||||||
def common(
|
def common(
|
||||||
format: cst.TStruct[ContainerType],
|
format: ContainerType,
|
||||||
datasample: Buffer,
|
datasample: Buffer,
|
||||||
objsample: t.Union[ContainerType, t.Dict[str, t.Any]],
|
objsample: t.Union[ContainerType, t.Dict[str, t.Any]],
|
||||||
sizesample: t.Union[int, t.Type[Exception]] = ...,
|
sizesample: t.Union[int, t.Type[Exception]] = ...,
|
||||||
|
|
|
||||||
|
|
@ -1,72 +1,80 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# pyright: strict
|
# pyright: strict
|
||||||
import dataclasses
|
|
||||||
import enum
|
|
||||||
import typing as t
|
import typing as t
|
||||||
|
|
||||||
import construct as cs
|
import construct as cs
|
||||||
import construct_typed as cst
|
from construct_typed import (
|
||||||
from construct_typed import DataclassBitStruct, DataclassMixin, DataclassStruct, csfield
|
DataclassBitStruct,
|
||||||
|
DataclassStruct,
|
||||||
|
csfield,
|
||||||
|
constr,
|
||||||
|
TEnum,
|
||||||
|
TFlags,
|
||||||
|
)
|
||||||
|
|
||||||
from .declarativeunittest import common, raises, setattrs
|
from tests.declarativeunittest import common, raises, setattrs
|
||||||
|
|
||||||
|
|
||||||
def test_dataclass_const_default() -> None:
|
def test_dataclass_const_default() -> None:
|
||||||
@dataclasses.dataclass
|
class TestDataclass(DataclassStruct):
|
||||||
class ConstDefaultTest(DataclassMixin):
|
const_bytes: bytes = csfield(cs.Bytes(3), const=b"BMP")
|
||||||
const_bytes: bytes = csfield(cs.Const(b"BMP"))
|
const_int: int = csfield(cs.Int8ub, const=5)
|
||||||
const_int: int = csfield(cs.Const(5, cs.Int8ub))
|
default_int: int = csfield(cs.Int8ub, default=26)
|
||||||
default_int: int = csfield(cs.Default(cs.Int8ub, 28))
|
default_lambda: t.Optional[bytes] = csfield(
|
||||||
default_lambda: bytes = csfield(
|
|
||||||
cs.Default(cs.Bytes(cs.this.const_int), lambda ctx: bytes(ctx.const_int))
|
cs.Default(cs.Bytes(cs.this.const_int), lambda ctx: bytes(ctx.const_int))
|
||||||
)
|
)
|
||||||
|
|
||||||
a = ConstDefaultTest()
|
obj = TestDataclass()
|
||||||
assert a.const_bytes == b"BMP"
|
assert obj.const_bytes == b"BMP"
|
||||||
assert a.const_int == 5
|
assert obj.const_int == 5
|
||||||
assert a.default_int == 28
|
assert obj.default_int == 26
|
||||||
assert a.default_lambda == None
|
assert obj.default_lambda == None
|
||||||
|
obj = TestDataclass(default_int=1)
|
||||||
|
assert obj.default_int == 1
|
||||||
|
|
||||||
|
fmt = TestDataclass.__constr__()
|
||||||
|
assert isinstance(fmt.const_bytes.subcon, cs.Const)
|
||||||
|
assert isinstance(fmt.const_int.subcon, cs.Const)
|
||||||
|
assert isinstance(fmt.default_int.subcon, cs.Default)
|
||||||
|
assert isinstance(fmt.default_lambda.subcon, cs.Default)
|
||||||
|
|
||||||
|
|
||||||
def test_dataclass_access() -> None:
|
def test_dataclass_access() -> None:
|
||||||
@dataclasses.dataclass
|
class TestDataclass(DataclassStruct):
|
||||||
class TestTContainer(DataclassMixin):
|
a: int = csfield(cs.Byte, const=1)
|
||||||
a: t.Optional[int] = csfield(cs.Const(1, cs.Byte))
|
|
||||||
b: int = csfield(cs.Int8ub)
|
b: int = csfield(cs.Int8ub)
|
||||||
|
|
||||||
tcontainer = TestTContainer(b=2)
|
obj = TestDataclass(b=2)
|
||||||
|
|
||||||
# tcontainer
|
assert obj.a == 1
|
||||||
assert tcontainer.a == 1
|
assert obj["a"] == 1
|
||||||
assert tcontainer["a"] == 1
|
assert obj.b == 2
|
||||||
assert tcontainer.b == 2
|
assert obj["b"] == 2
|
||||||
assert tcontainer["b"] == 2
|
|
||||||
|
|
||||||
tcontainer.a = 5
|
obj.a = 5
|
||||||
assert tcontainer.a == 5
|
assert obj.a == 5
|
||||||
assert tcontainer["a"] == 5
|
assert obj["a"] == 5
|
||||||
tcontainer["a"] = 6
|
obj["a"] = 6
|
||||||
assert tcontainer.a == 6
|
assert obj.a == 6
|
||||||
assert tcontainer["a"] == 6
|
assert obj["a"] == 6
|
||||||
|
|
||||||
# wrong creation
|
# wrong creation
|
||||||
assert raises(lambda: TestTContainer(a=0, b=1)) == TypeError
|
assert raises(lambda: TestDataclass(a=0, b=1)) == TypeError # type: ignore
|
||||||
|
|
||||||
|
|
||||||
def test_dataclass_str_repr() -> None:
|
def test_dataclass_str_repr() -> None:
|
||||||
@dataclasses.dataclass
|
class Image(DataclassStruct):
|
||||||
class Image(DataclassMixin):
|
signature: bytes = csfield(cs.Bytes(3), const=b"BMP")
|
||||||
signature: t.Optional[bytes] = csfield(cs.Const(b"BMP"))
|
|
||||||
width: int = csfield(cs.Int8ub)
|
width: int = csfield(cs.Int8ub)
|
||||||
height: int = csfield(cs.Int8ub)
|
height: int = csfield(cs.Int8ub)
|
||||||
|
|
||||||
format = DataclassStruct(Image)
|
fmt = constr(Image)
|
||||||
obj = Image(width=3, height=2)
|
obj = Image(width=3, height=2)
|
||||||
assert (
|
assert (
|
||||||
str(obj)
|
str(obj)
|
||||||
== "Image: \n signature = b'BMP' (total 3)\n width = 3\n height = 2"
|
== "Image: \n signature = b'BMP' (total 3)\n width = 3\n height = 2"
|
||||||
)
|
)
|
||||||
obj = format.parse(format.build(obj))
|
obj = fmt.parse(fmt.build(obj))
|
||||||
assert (
|
assert (
|
||||||
str(obj)
|
str(obj)
|
||||||
== "Image: \n signature = b'BMP' (total 3)\n width = 3\n height = 2"
|
== "Image: \n signature = b'BMP' (total 3)\n width = 3\n height = 2"
|
||||||
|
|
@ -74,64 +82,56 @@ def test_dataclass_str_repr() -> None:
|
||||||
|
|
||||||
|
|
||||||
def test_dataclass_struct() -> None:
|
def test_dataclass_struct() -> None:
|
||||||
@dataclasses.dataclass
|
class Image(DataclassStruct):
|
||||||
class Image(DataclassMixin):
|
|
||||||
width: int = csfield(cs.Int8ub)
|
width: int = csfield(cs.Int8ub)
|
||||||
height: int = csfield(cs.Int8ub)
|
height: int = csfield(cs.Int8ub)
|
||||||
pixels: bytes = csfield(cs.Bytes(cs.this.height * cs.this.width))
|
pixels: bytes = csfield(cs.Bytes(cs.this.height * cs.this.width))
|
||||||
|
|
||||||
common(
|
common(
|
||||||
cst.DataclassStruct(Image),
|
constr(Image),
|
||||||
b"\x01\x0212",
|
b"\x01\x0212",
|
||||||
Image(width=1, height=2, pixels=b"12"),
|
Image(width=1, height=2, pixels=b"12"),
|
||||||
)
|
)
|
||||||
|
|
||||||
# check __getattr__
|
# check __getattr__
|
||||||
c = cst.DataclassStruct(Image)
|
fmt = Image.__constr__()
|
||||||
assert c.width.name == "width"
|
assert fmt.width.name == "width"
|
||||||
assert c.height.name == "height"
|
assert fmt.height.name == "height"
|
||||||
assert c.width.subcon is cs.Int8ub
|
assert fmt.width.subcon is cs.Int8ub
|
||||||
assert c.height.subcon is cs.Int8ub
|
assert fmt.height.subcon is cs.Int8ub
|
||||||
|
|
||||||
|
|
||||||
def test_dataclass_struct_reverse() -> None:
|
def test_dataclass_struct_reverse() -> None:
|
||||||
@dataclasses.dataclass
|
class TestDataclass(DataclassStruct, reverse_fields=True):
|
||||||
class TestContainer(DataclassMixin):
|
|
||||||
a: int = csfield(cs.Int16ub)
|
a: int = csfield(cs.Int16ub)
|
||||||
b: int = csfield(cs.Int8ub)
|
b: int = csfield(cs.Int8ub)
|
||||||
|
|
||||||
common(
|
common(
|
||||||
DataclassStruct(TestContainer, reverse=True),
|
constr(TestDataclass),
|
||||||
b"\x02\x00\x01",
|
b"\x02\x00\x01",
|
||||||
TestContainer(a=1, b=2),
|
TestDataclass(a=1, b=2),
|
||||||
3,
|
3,
|
||||||
)
|
)
|
||||||
normal = DataclassStruct(TestContainer)
|
|
||||||
reverse = DataclassStruct(TestContainer, reverse=True)
|
|
||||||
assert str(normal.parse(b"\x00\x01\x02")) == str(reverse.parse(b"\x02\x00\x01"))
|
|
||||||
|
|
||||||
|
|
||||||
def test_dataclass_struct_nested() -> None:
|
def test_dataclass_struct_nested() -> None:
|
||||||
@dataclasses.dataclass
|
class TestDataclass(DataclassStruct):
|
||||||
class TestContainer(DataclassMixin):
|
class InnerDataclass(DataclassStruct):
|
||||||
@dataclasses.dataclass
|
|
||||||
class InnerDataclass(DataclassMixin):
|
|
||||||
b: int = csfield(cs.Byte)
|
b: int = csfield(cs.Byte)
|
||||||
c: bytes = csfield(cs.Bytes(cs.this._.length))
|
c: bytes = csfield(cs.Bytes(cs.this._.length))
|
||||||
|
|
||||||
length: int = csfield(cs.Byte)
|
length: int = csfield(cs.Byte)
|
||||||
a: InnerDataclass = csfield(DataclassStruct(InnerDataclass))
|
a: InnerDataclass = csfield(constr(InnerDataclass))
|
||||||
|
|
||||||
common(
|
common(
|
||||||
DataclassStruct(TestContainer),
|
constr(TestDataclass),
|
||||||
b"\x02\x01\xF1\xF2",
|
b"\x02\x01\xF1\xF2",
|
||||||
TestContainer(length=2, a=TestContainer.InnerDataclass(b=1, c=b"\xF1\xF2")),
|
TestDataclass(length=2, a=TestDataclass.InnerDataclass(b=1, c=b"\xF1\xF2")),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_dataclass_struct_default_field() -> None:
|
def test_dataclass_struct_default_field() -> None:
|
||||||
@dataclasses.dataclass
|
class Image(DataclassStruct):
|
||||||
class Image(DataclassMixin):
|
|
||||||
width: int = csfield(cs.Int8ub)
|
width: int = csfield(cs.Int8ub)
|
||||||
height: int = csfield(cs.Int8ub)
|
height: int = csfield(cs.Int8ub)
|
||||||
pixels: t.Optional[bytes] = csfield(
|
pixels: t.Optional[bytes] = csfield(
|
||||||
|
|
@ -142,80 +142,75 @@ def test_dataclass_struct_default_field() -> None:
|
||||||
)
|
)
|
||||||
|
|
||||||
common(
|
common(
|
||||||
DataclassStruct(Image),
|
constr(Image),
|
||||||
b"\x02\x03\x00\x00\x00\x00\x00\x00",
|
b"\x02\x03\x00\x00\x00\x00\x00\x00",
|
||||||
setattrs(Image(2, 3), pixels=bytes(6)),
|
setattrs(Image(width=2, height=3), pixels=bytes(6)),
|
||||||
sample_building=Image(2, 3),
|
sample_building=Image(width=2, height=3),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_dataclass_struct_const_field() -> None:
|
def test_dataclass_struct_const_field() -> None:
|
||||||
@dataclasses.dataclass
|
class TestDataclass(DataclassStruct):
|
||||||
class TestContainer(DataclassMixin):
|
|
||||||
const_field: t.Optional[bytes] = csfield(cs.Const(b"\x00"))
|
const_field: t.Optional[bytes] = csfield(cs.Const(b"\x00"))
|
||||||
|
|
||||||
common(
|
common(
|
||||||
DataclassStruct(TestContainer),
|
constr(TestDataclass),
|
||||||
bytes(1),
|
bytes(1),
|
||||||
setattrs(TestContainer(), const_field=b"\x00"),
|
setattrs(TestDataclass(), const_field=b"\x00"),
|
||||||
1,
|
1,
|
||||||
)
|
)
|
||||||
|
|
||||||
assert (
|
assert (
|
||||||
raises(
|
raises(
|
||||||
DataclassStruct(TestContainer).build,
|
constr(TestDataclass).build,
|
||||||
setattrs(TestContainer(), const_field=b"\x01"),
|
setattrs(TestDataclass(), const_field=b"\x01"),
|
||||||
)
|
)
|
||||||
== cs.ConstError
|
== cs.ConstError
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_dataclass_struct_array_field() -> None:
|
def test_dataclass_struct_array_field() -> None:
|
||||||
@dataclasses.dataclass
|
class TestDataclass(DataclassStruct):
|
||||||
class TestContainer(DataclassMixin):
|
|
||||||
array_field: t.List[int] = csfield(cs.Array(5, cs.Int8ub))
|
array_field: t.List[int] = csfield(cs.Array(5, cs.Int8ub))
|
||||||
|
|
||||||
common(
|
common(
|
||||||
DataclassStruct(TestContainer),
|
constr(TestDataclass),
|
||||||
bytes(5),
|
bytes(5),
|
||||||
TestContainer(array_field=[0, 0, 0, 0, 0]),
|
TestDataclass(array_field=[0, 0, 0, 0, 0]),
|
||||||
5,
|
5,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_dataclass_struct_anonymus_fields_1() -> None:
|
def test_dataclass_struct_anonymus_fields_1() -> None:
|
||||||
@dataclasses.dataclass
|
class TestDataclass(DataclassStruct):
|
||||||
class TestContainer(DataclassMixin):
|
|
||||||
_1: t.Optional[bytes] = csfield(cs.Const(b"\x00"))
|
_1: t.Optional[bytes] = csfield(cs.Const(b"\x00"))
|
||||||
_2: None = csfield(cs.Padding(1))
|
_2: None = csfield(cs.Padding(1))
|
||||||
_3: None = csfield(cs.Pass)
|
_3: None = csfield(cs.Pass)
|
||||||
_4: None = csfield(cs.Terminated)
|
_4: None = csfield(cs.Terminated)
|
||||||
|
|
||||||
common(
|
common(
|
||||||
DataclassStruct(TestContainer),
|
constr(TestDataclass),
|
||||||
bytes(2),
|
bytes(2),
|
||||||
setattrs(TestContainer(), _1=b"\x00"),
|
setattrs(TestDataclass(), _1=b"\x00"),
|
||||||
cs.SizeofError,
|
cs.SizeofError,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_dataclass_struct_anonymus_fields_2() -> None:
|
def test_dataclass_struct_anonymus_fields_2() -> None:
|
||||||
@dataclasses.dataclass
|
class TestDataclass(DataclassStruct):
|
||||||
class TestContainer(DataclassMixin):
|
_1: t.Optional[int] = csfield(cs.Computed(7))
|
||||||
_1: int = csfield(cs.Computed(7))
|
|
||||||
_2: t.Optional[bytes] = csfield(cs.Const(b"JPEG"))
|
_2: t.Optional[bytes] = csfield(cs.Const(b"JPEG"))
|
||||||
_3: None = csfield(cs.Pass)
|
_3: None = csfield(cs.Pass)
|
||||||
_4: None = csfield(cs.Terminated)
|
_4: None = csfield(cs.Terminated)
|
||||||
|
|
||||||
d = DataclassStruct(TestContainer)
|
fmt = constr(TestDataclass)
|
||||||
assert d.build(TestContainer()) == d.build(TestContainer())
|
assert fmt.build(TestDataclass()) == fmt.build(TestDataclass())
|
||||||
|
|
||||||
|
|
||||||
def test_dataclass_struct_overloaded_method() -> None:
|
def test_dataclass_struct_overloaded_method() -> None:
|
||||||
# Test dot access to some names that are not accessable via dot
|
# Test dot access to some names that are not accessable via dot
|
||||||
# in the original 'cs.Container'.
|
# in the original 'cs.Container'.
|
||||||
@dataclasses.dataclass
|
class TestDataclass(DataclassStruct):
|
||||||
class TestContainer(DataclassMixin):
|
|
||||||
clear: int = csfield(cs.Int8ul)
|
clear: int = csfield(cs.Int8ul)
|
||||||
copy: int = csfield(cs.Int8ul)
|
copy: int = csfield(cs.Int8ul)
|
||||||
fromkeys: int = csfield(cs.Int8ul)
|
fromkeys: int = csfield(cs.Int8ul)
|
||||||
|
|
@ -231,10 +226,10 @@ def test_dataclass_struct_overloaded_method() -> None:
|
||||||
update: int = csfield(cs.Int8ul)
|
update: int = csfield(cs.Int8ul)
|
||||||
values: int = csfield(cs.Int8ul)
|
values: int = csfield(cs.Int8ul)
|
||||||
|
|
||||||
d = DataclassStruct(TestContainer)
|
fmt = constr(TestDataclass)
|
||||||
obj = d.parse(
|
obj = fmt.parse(
|
||||||
d.build(
|
fmt.build(
|
||||||
TestContainer(
|
TestDataclass(
|
||||||
clear=1,
|
clear=1,
|
||||||
copy=2,
|
copy=2,
|
||||||
fromkeys=3,
|
fromkeys=3,
|
||||||
|
|
@ -268,169 +263,172 @@ def test_dataclass_struct_overloaded_method() -> None:
|
||||||
assert obj.values == 14
|
assert obj.values == 14
|
||||||
|
|
||||||
|
|
||||||
def test_dataclass_struct_no_dataclass() -> None:
|
|
||||||
class TestContainer(DataclassMixin):
|
|
||||||
a: int = csfield(cs.Int16ub)
|
|
||||||
b: int = csfield(cs.Int8ub)
|
|
||||||
|
|
||||||
assert raises(lambda: DataclassStruct(TestContainer)) == TypeError
|
|
||||||
|
|
||||||
|
|
||||||
def test_dataclass_struct_no_DataclassMixin() -> None:
|
|
||||||
@dataclasses.dataclass
|
|
||||||
class TestContainer:
|
|
||||||
a: int = csfield(cs.Int16ub)
|
|
||||||
b: int = csfield(cs.Int8ub)
|
|
||||||
|
|
||||||
cls = t.cast(t.Type[DataclassMixin], TestContainer)
|
|
||||||
assert raises(lambda: DataclassStruct(cls)) == TypeError
|
|
||||||
|
|
||||||
|
|
||||||
def test_dataclass_struct_wrong_container() -> None:
|
def test_dataclass_struct_wrong_container() -> None:
|
||||||
@dataclasses.dataclass
|
class TestContainer1(DataclassStruct):
|
||||||
class TestContainer1(DataclassMixin):
|
|
||||||
a: int = csfield(cs.Int16ub)
|
a: int = csfield(cs.Int16ub)
|
||||||
b: int = csfield(cs.Int8ub)
|
b: int = csfield(cs.Int8ub)
|
||||||
|
|
||||||
@dataclasses.dataclass
|
class TestContainer2(DataclassStruct):
|
||||||
class TestContainer2(DataclassMixin):
|
|
||||||
a: int = csfield(cs.Int16ub)
|
a: int = csfield(cs.Int16ub)
|
||||||
b: int = csfield(cs.Int8ub)
|
b: int = csfield(cs.Int8ub)
|
||||||
|
|
||||||
assert (
|
assert raises(constr(TestContainer1).build, TestContainer2(a=1, b=2)) == TypeError
|
||||||
raises(DataclassStruct(TestContainer1).build, TestContainer2(a=1, b=2))
|
|
||||||
== TypeError
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def test_dataclass_struct_doc() -> None:
|
def test_dataclass_struct_doc() -> None:
|
||||||
@dataclasses.dataclass
|
class TestDataclass1(DataclassStruct):
|
||||||
class TestContainer(DataclassMixin):
|
"""
|
||||||
a: int = csfield(cs.Int16ub, "This is the documentation of a")
|
Documentation of TestDataclass1
|
||||||
b: int = csfield(
|
"""
|
||||||
cs.Int8ub, doc="This is the documentation of b\nwhich is multiline"
|
|
||||||
)
|
a: int = csfield(cs.Int16ub, doc="This is the doc of a")
|
||||||
|
b: int = csfield(cs.Int8ub, doc="This is the doc of b\nwhich is multiline")
|
||||||
c: int = csfield(
|
c: int = csfield(
|
||||||
cs.Int8ub,
|
cs.Int8ub,
|
||||||
"""
|
doc="""
|
||||||
This is the documentation of c
|
This is the doc of c
|
||||||
which is also multiline
|
which is also multiline
|
||||||
""",
|
""",
|
||||||
)
|
)
|
||||||
|
|
||||||
format = DataclassStruct(TestContainer)
|
fmt1 = TestDataclass1.__constr__()
|
||||||
common(format, b"\x00\x01\x02\x03", TestContainer(a=1, b=2, c=3), 4)
|
common(fmt1, b"\x00\x01\x02\x03", TestDataclass1(a=1, b=2, c=3), 4)
|
||||||
|
|
||||||
assert format.subcon.a.docs == "This is the documentation of a"
|
assert fmt1.docs == "Documentation of TestDataclass1"
|
||||||
assert format.subcon.b.docs == "This is the documentation of b\nwhich is multiline"
|
assert fmt1.subcon.a.docs == "This is the doc of a"
|
||||||
assert (
|
assert fmt1.subcon.b.docs == "This is the doc of b\nwhich is multiline"
|
||||||
format.subcon.c.docs
|
assert fmt1.subcon.c.docs == "This is the doc of c\nwhich is also multiline"
|
||||||
== "This is the documentation of c\nwhich is also multiline"
|
|
||||||
)
|
class TestDataclass2(DataclassStruct):
|
||||||
|
a: int = csfield(cs.Int16ub)
|
||||||
|
b: int = csfield(cs.Int8ub)
|
||||||
|
c: int = csfield(cs.Int8ub)
|
||||||
|
|
||||||
|
fmt2 = TestDataclass2.__constr__()
|
||||||
|
assert fmt2.docs == ""
|
||||||
|
assert fmt2.subcon.a.docs == ""
|
||||||
|
assert fmt2.subcon.b.docs == ""
|
||||||
|
assert fmt2.subcon.c.docs == ""
|
||||||
|
|
||||||
|
|
||||||
def test_dataclass_bitstruct() -> None:
|
def test_dataclass_bitwise() -> None:
|
||||||
@dataclasses.dataclass
|
class TestDataclass(DataclassStruct, constr=lambda cls: cs.Bitwise(cls)):
|
||||||
class TestContainer(DataclassMixin):
|
|
||||||
a: int = csfield(cs.BitsInteger(7))
|
a: int = csfield(cs.BitsInteger(7))
|
||||||
b: int = csfield(cs.Bit)
|
b: int = csfield(cs.Bit)
|
||||||
c: int = csfield(cs.BitsInteger(8))
|
c: int = csfield(cs.BitsInteger(8))
|
||||||
|
|
||||||
print("")
|
|
||||||
|
|
||||||
common(
|
common(
|
||||||
DataclassBitStruct(TestContainer),
|
constr(TestDataclass),
|
||||||
b"\xFD\x12",
|
b"\xFD\x12",
|
||||||
TestContainer(a=0x7E, b=1, c=0x12),
|
TestDataclass(a=0x7E, b=1, c=0x12),
|
||||||
2,
|
2,
|
||||||
)
|
)
|
||||||
|
|
||||||
# check __getattr__
|
# check __getattr__
|
||||||
c = DataclassStruct(TestContainer)
|
fmt = TestDataclass.__constr__()
|
||||||
assert c.a.name == "a"
|
assert fmt.subcon.a.name == "a"
|
||||||
assert c.b.name == "b"
|
assert fmt.subcon.b.name == "b"
|
||||||
assert c.c.name == "c"
|
assert fmt.subcon.c.name == "c"
|
||||||
assert isinstance(c.a.subcon, cs.BitsInteger)
|
assert isinstance(fmt.subcon.a.subcon, cs.BitsInteger)
|
||||||
assert c.b.subcon is cs.Bit
|
assert fmt.subcon.b.subcon is cs.Bit
|
||||||
assert isinstance(c.c.subcon, cs.BitsInteger)
|
assert isinstance(fmt.subcon.c.subcon, cs.BitsInteger)
|
||||||
|
|
||||||
|
|
||||||
|
def test_dataclass_bitstruct() -> None:
|
||||||
|
class TestDataclass(DataclassBitStruct):
|
||||||
|
a: int = csfield(cs.BitsInteger(7))
|
||||||
|
b: int = csfield(cs.Bit)
|
||||||
|
c: int = csfield(cs.BitsInteger(8))
|
||||||
|
|
||||||
|
common(
|
||||||
|
constr(TestDataclass),
|
||||||
|
b"\xFD\x12",
|
||||||
|
TestDataclass(a=0x7E, b=1, c=0x12),
|
||||||
|
2,
|
||||||
|
)
|
||||||
|
|
||||||
|
# check __getattr__
|
||||||
|
fmt = TestDataclass.__constr__()
|
||||||
|
assert fmt.subcon.a.name == "a"
|
||||||
|
assert fmt.subcon.b.name == "b"
|
||||||
|
assert fmt.subcon.c.name == "c"
|
||||||
|
assert isinstance(fmt.subcon.a.subcon, cs.BitsInteger)
|
||||||
|
assert fmt.subcon.b.subcon is cs.Bit
|
||||||
|
assert isinstance(fmt.subcon.c.subcon, cs.BitsInteger)
|
||||||
|
|
||||||
|
|
||||||
def test_tenum() -> None:
|
def test_tenum() -> None:
|
||||||
class TestEnum(cst.EnumBase):
|
class TestEnum(TEnum, subcon=cs.Byte):
|
||||||
one = 1
|
one = 1
|
||||||
two = 2
|
two = 2
|
||||||
four = 4
|
four = 4
|
||||||
eight = 8
|
eight = 8
|
||||||
|
|
||||||
d = cst.TEnum(cs.Byte, TestEnum)
|
fmt = constr(TestEnum)
|
||||||
|
|
||||||
common(d, b"\x01", TestEnum.one, 1)
|
common(fmt, b"\x01", TestEnum.one, 1)
|
||||||
common(d, b"\xff", TestEnum(255), 1)
|
common(fmt, b"\xff", TestEnum(255), 1)
|
||||||
assert d.parse(b"\x01") == TestEnum.one
|
assert fmt.parse(b"\x01") == TestEnum.one
|
||||||
assert d.parse(b"\x01") == 1
|
assert fmt.parse(b"\x01") == 1
|
||||||
assert int(d.parse(b"\x01")) == 1
|
assert int(fmt.parse(b"\x01")) == 1
|
||||||
assert d.parse(b"\xff") == TestEnum(255)
|
assert fmt.parse(b"\xff") == TestEnum(255)
|
||||||
assert d.parse(b"\xff") == 255
|
assert fmt.parse(b"\xff") == 255
|
||||||
assert int(d.parse(b"\xff")) == 255
|
assert int(fmt.parse(b"\xff")) == 255
|
||||||
assert raises(d.build, 8) == TypeError
|
assert raises(fmt.build, 8) == TypeError
|
||||||
|
|
||||||
|
|
||||||
def test_tenum_no_enumbase() -> None:
|
def test_tenum_doc() -> None:
|
||||||
class E(enum.Enum):
|
class TestEnum1(TEnum, subcon=cs.Byte):
|
||||||
|
"""
|
||||||
|
TestEnum documentation
|
||||||
|
"""
|
||||||
|
|
||||||
|
one = 1
|
||||||
|
|
||||||
|
d1 = constr(TestEnum1)
|
||||||
|
assert d1.docs == "TestEnum documentation"
|
||||||
|
|
||||||
|
class TestEnum2(TEnum, subcon=cs.Byte):
|
||||||
|
two = 2
|
||||||
|
|
||||||
|
d2 = constr(TestEnum2)
|
||||||
|
assert d2.docs == ""
|
||||||
|
|
||||||
|
|
||||||
|
def test_tenum_in_dataclass_struct() -> None:
|
||||||
|
class TestEnum(TEnum, subcon=cs.Int8ub):
|
||||||
a = 1
|
a = 1
|
||||||
b = 2
|
b = 2
|
||||||
|
|
||||||
cls = t.cast(t.Type[cst.EnumBase], E)
|
class TestDataclass(DataclassStruct):
|
||||||
assert raises(lambda: cst.TEnum(cs.Byte, cls)) == TypeError
|
a: TestEnum = csfield(constr(TestEnum))
|
||||||
|
|
||||||
|
|
||||||
def test_dataclass_struct_wrong_enumbase() -> None:
|
|
||||||
class E1(cst.EnumBase):
|
|
||||||
a = 1
|
|
||||||
b = 2
|
|
||||||
|
|
||||||
class E2(cst.EnumBase):
|
|
||||||
a = 1
|
|
||||||
b = 2
|
|
||||||
|
|
||||||
assert raises(cst.TEnum(cs.Byte, E1).build, E2.a) == TypeError
|
|
||||||
|
|
||||||
|
|
||||||
def test_tenum_in_tstruct() -> None:
|
|
||||||
class TestEnum(cst.EnumBase):
|
|
||||||
a = 1
|
|
||||||
b = 2
|
|
||||||
|
|
||||||
@dataclasses.dataclass
|
|
||||||
class TestContainer(DataclassMixin):
|
|
||||||
a: TestEnum = csfield(cst.TEnum(cs.Int8ub, TestEnum))
|
|
||||||
b: int = csfield(cs.Int8ub)
|
b: int = csfield(cs.Int8ub)
|
||||||
|
|
||||||
common(
|
common(
|
||||||
DataclassStruct(TestContainer),
|
constr(TestDataclass),
|
||||||
b"\x01\x02",
|
b"\x01\x02",
|
||||||
TestContainer(a=TestEnum.a, b=2),
|
TestDataclass(a=TestEnum.a, b=2),
|
||||||
2,
|
2,
|
||||||
)
|
)
|
||||||
|
|
||||||
assert (
|
assert (
|
||||||
raises(cst.TEnum(cs.Byte, TestEnum).build, TestContainer(a=1, b=2)) == TypeError # type: ignore
|
raises(constr(TestEnum).build, TestDataclass(a=1, b=2)) == TypeError # type: ignore
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_tenum_flags() -> None:
|
def test_tflags() -> None:
|
||||||
class TestEnum(cst.FlagsEnumBase):
|
class TestFlags(TFlags, subcon=cs.Byte):
|
||||||
one = 1
|
one = 1
|
||||||
two = 2
|
two = 2
|
||||||
four = 4
|
four = 4
|
||||||
eight = 8
|
eight = 8
|
||||||
|
|
||||||
d = cst.TFlagsEnum(cs.Byte, TestEnum)
|
fmt = constr(TestFlags)
|
||||||
common(d, b"\x03", TestEnum.one | TestEnum.two, 1)
|
common(fmt, b"\x03", TestFlags.one | TestFlags.two, 1)
|
||||||
assert d.build(TestEnum(0)) == b"\x00"
|
assert fmt.build(TestFlags(0)) == b"\x00"
|
||||||
assert d.build(TestEnum.one | TestEnum.two) == b"\x03"
|
assert fmt.build(TestFlags.one | TestFlags.two) == b"\x03"
|
||||||
assert d.build(TestEnum(8)) == b"\x08"
|
assert fmt.build(TestFlags(8)) == b"\x08"
|
||||||
assert d.build(TestEnum(1 | 2)) == b"\x03"
|
assert fmt.build(TestFlags(1 | 2)) == b"\x03"
|
||||||
assert d.build(TestEnum(255)) == b"\xff"
|
assert fmt.build(TestFlags(255)) == b"\xff"
|
||||||
assert d.build(TestEnum.eight) == b"\x08"
|
assert fmt.build(TestFlags.eight) == b"\x08"
|
||||||
assert raises(d.build, 2) == TypeError
|
assert raises(fmt.build, 2) == TypeError
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue