Compare commits
13 commits
main
...
feature/ne
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
75dbd8d822 | ||
|
|
be5ae240fb | ||
|
|
94a8097bec | ||
|
|
e99dd5d752 | ||
|
|
8525c04165 | ||
|
|
7c68aeecd4 | ||
|
|
96ef565044 | ||
|
|
de76415ac5 | ||
|
|
9a9b4ab95a | ||
|
|
8169f0ed31 | ||
|
|
b896457f90 | ||
|
|
753e4282ee | ||
|
|
db9b35a0c2 |
9 changed files with 473 additions and 377 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,11 @@
|
||||||
from .dataclass_struct import (
|
from construct_typed.generic import constr
|
||||||
|
from construct_typed.dataclass_struct import (
|
||||||
DataclassBitStruct,
|
DataclassBitStruct,
|
||||||
DataclassMixin,
|
|
||||||
DataclassStruct,
|
DataclassStruct,
|
||||||
TBitStruct,
|
|
||||||
TContainerBase,
|
|
||||||
TContainerMixin,
|
|
||||||
TStruct,
|
|
||||||
TStructField,
|
|
||||||
csfield,
|
csfield,
|
||||||
sfield,
|
this_struct
|
||||||
)
|
)
|
||||||
from .generic_wrapper import (
|
from construct_typed.generic import (
|
||||||
Adapter,
|
Adapter,
|
||||||
ConstantOrContextLambda,
|
ConstantOrContextLambda,
|
||||||
Construct,
|
Construct,
|
||||||
|
|
@ -18,23 +13,18 @@ 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",
|
||||||
|
"this_struct",
|
||||||
"Adapter",
|
"Adapter",
|
||||||
"ConstantOrContextLambda",
|
"ConstantOrContextLambda",
|
||||||
"Construct",
|
"Construct",
|
||||||
|
|
|
||||||
|
|
@ -12,21 +12,249 @@ 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
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
DATACLASS_METADATA_KEY = "__construct_typed_subcon"
|
||||||
|
|
||||||
|
# specialisation for constructs, that builds from none and dont have to be declared in the __init__ method
|
||||||
|
@t.overload
|
||||||
|
def csfield(
|
||||||
|
subcon: "cs.Construct[ParsedType, None]",
|
||||||
|
doc: t.Optional[str] = None,
|
||||||
|
parsed: t.Optional[t.Callable[[t.Any, Context], None]] = None,
|
||||||
|
init: t.Literal[False] = False,
|
||||||
|
) -> ParsedType:
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
|
@t.overload
|
||||||
|
def csfield(
|
||||||
|
subcon: "Construct[ParsedType, t.Any]",
|
||||||
|
doc: t.Optional[str] = None,
|
||||||
|
parsed: t.Optional[t.Callable[[t.Any, Context], None]] = None,
|
||||||
|
init: bool = True,
|
||||||
|
) -> ParsedType:
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
|
def csfield(
|
||||||
|
subcon: "Construct[ParsedType, t.Any]",
|
||||||
|
doc: t.Optional[str] = None,
|
||||||
|
parsed: t.Optional[t.Callable[[t.Any, Context], None]] = None,
|
||||||
|
init: bool = True,
|
||||||
|
) -> 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),
|
orig_subcon = subcon
|
||||||
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
|
# Rename subcon, if doc or parsed are available
|
||||||
methods exists and every name can be used.
|
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 subcons
|
||||||
|
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={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
|
||||||
|
|
||||||
|
|
||||||
|
# Helper object for defining the `constr` of a `struct`. Will be replaced with the proper construct, when class is created.
|
||||||
|
this_struct: Construct[t.Any, t.Any] = Construct()
|
||||||
|
|
||||||
|
|
||||||
|
def _replace_this_struct(constr: "Construct[t.Any, t.Any]", replacement: t.Any) -> None:
|
||||||
|
"""Recursive search for `this_struct` in all SubConstructs and replace it with AttrsStruct"""
|
||||||
|
subcon = getattr(constr, "subcon", None)
|
||||||
|
if subcon is this_struct:
|
||||||
|
setattr(constr, "subcon", replacement)
|
||||||
|
elif subcon is not None:
|
||||||
|
_replace_this_struct(subcon, replacement)
|
||||||
|
else:
|
||||||
|
raise ValueError(
|
||||||
|
"Could not find `this_struct`. Only SubConstructs are supported"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@__dataclass_transform__(field_descriptors=(csfield,))
|
||||||
|
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.
|
||||||
|
|
||||||
|
:param constr: This can be used if the structure is nested inside a Subconstruct. To represent this struct use the constant `this_struct`.
|
||||||
|
: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,
|
||||||
|
constr: "cs.Construct[t.Any, t.Any]" = this_struct,
|
||||||
|
reverse_fields: bool = False,
|
||||||
|
) -> None:
|
||||||
|
# validate types
|
||||||
|
if not isinstance(constr, cs.Construct): # type: ignore
|
||||||
|
raise ValueError("`constr` parameter has to be an `Construct` object")
|
||||||
|
if not isinstance(reverse_fields, bool): # type: ignore
|
||||||
|
raise ValueError("`reverse_fields` parameter has to be an `bool` object")
|
||||||
|
|
||||||
|
# create dataclass
|
||||||
|
cls = dataclasses.dataclass(cls)
|
||||||
|
|
||||||
|
# create construct format
|
||||||
|
dc_constr = DataclassConstruct(cls, reverse_fields)
|
||||||
|
if constr is this_struct:
|
||||||
|
constr = dc_constr
|
||||||
|
else:
|
||||||
|
_replace_this_struct(constr, dc_constr)
|
||||||
|
|
||||||
|
# save construct format and make the class compatible to `Constructable` protocol
|
||||||
|
setattr(cls, "__constr__", lambda: 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 +300,41 @@ 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,
|
||||||
TBitStruct = DataclassBitStruct
|
constr: "cs.Construct[t.Any, t.Any]" = this_struct,
|
||||||
TContainerMixin = DataclassMixin
|
reverse_fields: bool = False,
|
||||||
TContainerBase = DataclassMixin
|
) -> None:
|
||||||
TStructField = csfield
|
cls = DataclassStruct.__init_subclass__.__func__(cls, cs.Bitwise(constr), reverse_fields) # type: ignore
|
||||||
sfield = csfield
|
|
||||||
|
|
|
||||||
|
|
@ -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,97 @@
|
||||||
import enum
|
import enum
|
||||||
import typing as t
|
import typing as t
|
||||||
|
|
||||||
from .generic_wrapper import *
|
import construct as cs
|
||||||
|
|
||||||
|
from .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:
|
||||||
|
# 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:
|
||||||
|
enum_constr = None
|
||||||
|
|
||||||
|
# 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 +103,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 +115,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 +146,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,19 +1,22 @@
|
||||||
# -*- 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 .declarativeunittest import common, raises, setattrs
|
||||||
|
|
||||||
|
|
||||||
def test_dataclass_const_default() -> None:
|
def test_dataclass_const_default() -> None:
|
||||||
@dataclasses.dataclass
|
class ConstDefaultTest(DataclassStruct):
|
||||||
class ConstDefaultTest(DataclassMixin):
|
|
||||||
const_bytes: bytes = csfield(cs.Const(b"BMP"))
|
const_bytes: bytes = csfield(cs.Const(b"BMP"))
|
||||||
const_int: int = csfield(cs.Const(5, cs.Int8ub))
|
const_int: int = csfield(cs.Const(5, cs.Int8ub))
|
||||||
default_int: int = csfield(cs.Default(cs.Int8ub, 28))
|
default_int: int = csfield(cs.Default(cs.Int8ub, 28))
|
||||||
|
|
@ -29,8 +32,7 @@ def test_dataclass_const_default() -> None:
|
||||||
|
|
||||||
|
|
||||||
def test_dataclass_access() -> None:
|
def test_dataclass_access() -> None:
|
||||||
@dataclasses.dataclass
|
class TestTContainer(DataclassStruct):
|
||||||
class TestTContainer(DataclassMixin):
|
|
||||||
a: t.Optional[int] = csfield(cs.Const(1, cs.Byte))
|
a: t.Optional[int] = csfield(cs.Const(1, cs.Byte))
|
||||||
b: int = csfield(cs.Int8ub)
|
b: int = csfield(cs.Int8ub)
|
||||||
|
|
||||||
|
|
@ -50,17 +52,16 @@ def test_dataclass_access() -> None:
|
||||||
assert tcontainer["a"] == 6
|
assert tcontainer["a"] == 6
|
||||||
|
|
||||||
# wrong creation
|
# wrong creation
|
||||||
assert raises(lambda: TestTContainer(a=0, b=1)) == TypeError
|
assert raises(lambda: TestTContainer(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: t.Optional[bytes] = csfield(cs.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)
|
format = constr(Image)
|
||||||
obj = Image(width=3, height=2)
|
obj = Image(width=3, height=2)
|
||||||
assert (
|
assert (
|
||||||
str(obj)
|
str(obj)
|
||||||
|
|
@ -74,20 +75,19 @@ 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)
|
c = Image.__constr__()
|
||||||
assert c.width.name == "width"
|
assert c.width.name == "width"
|
||||||
assert c.height.name == "height"
|
assert c.height.name == "height"
|
||||||
assert c.width.subcon is cs.Int8ub
|
assert c.width.subcon is cs.Int8ub
|
||||||
|
|
@ -95,43 +95,36 @@ def test_dataclass_struct() -> None:
|
||||||
|
|
||||||
|
|
||||||
def test_dataclass_struct_reverse() -> None:
|
def test_dataclass_struct_reverse() -> None:
|
||||||
@dataclasses.dataclass
|
class TestContainer(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(TestContainer),
|
||||||
b"\x02\x00\x01",
|
b"\x02\x00\x01",
|
||||||
TestContainer(a=1, b=2),
|
TestContainer(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 TestContainer(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(TestContainer),
|
||||||
b"\x02\x01\xF1\xF2",
|
b"\x02\x01\xF1\xF2",
|
||||||
TestContainer(length=2, a=TestContainer.InnerDataclass(b=1, c=b"\xF1\xF2")),
|
TestContainer(length=2, a=TestContainer.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,7 +135,7 @@ 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(2, 3), pixels=bytes(6)),
|
||||||
sample_building=Image(2, 3),
|
sample_building=Image(2, 3),
|
||||||
|
|
@ -150,12 +143,11 @@ def test_dataclass_struct_default_field() -> None:
|
||||||
|
|
||||||
|
|
||||||
def test_dataclass_struct_const_field() -> None:
|
def test_dataclass_struct_const_field() -> None:
|
||||||
@dataclasses.dataclass
|
class TestContainer(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(TestContainer),
|
||||||
bytes(1),
|
bytes(1),
|
||||||
setattrs(TestContainer(), const_field=b"\x00"),
|
setattrs(TestContainer(), const_field=b"\x00"),
|
||||||
1,
|
1,
|
||||||
|
|
@ -163,7 +155,7 @@ def test_dataclass_struct_const_field() -> None:
|
||||||
|
|
||||||
assert (
|
assert (
|
||||||
raises(
|
raises(
|
||||||
DataclassStruct(TestContainer).build,
|
constr(TestContainer).build,
|
||||||
setattrs(TestContainer(), const_field=b"\x01"),
|
setattrs(TestContainer(), const_field=b"\x01"),
|
||||||
)
|
)
|
||||||
== cs.ConstError
|
== cs.ConstError
|
||||||
|
|
@ -171,12 +163,11 @@ def test_dataclass_struct_const_field() -> None:
|
||||||
|
|
||||||
|
|
||||||
def test_dataclass_struct_array_field() -> None:
|
def test_dataclass_struct_array_field() -> None:
|
||||||
@dataclasses.dataclass
|
class TestContainer(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(TestContainer),
|
||||||
bytes(5),
|
bytes(5),
|
||||||
TestContainer(array_field=[0, 0, 0, 0, 0]),
|
TestContainer(array_field=[0, 0, 0, 0, 0]),
|
||||||
5,
|
5,
|
||||||
|
|
@ -184,15 +175,14 @@ def test_dataclass_struct_array_field() -> None:
|
||||||
|
|
||||||
|
|
||||||
def test_dataclass_struct_anonymus_fields_1() -> None:
|
def test_dataclass_struct_anonymus_fields_1() -> None:
|
||||||
@dataclasses.dataclass
|
class TestContainer(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(TestContainer),
|
||||||
bytes(2),
|
bytes(2),
|
||||||
setattrs(TestContainer(), _1=b"\x00"),
|
setattrs(TestContainer(), _1=b"\x00"),
|
||||||
cs.SizeofError,
|
cs.SizeofError,
|
||||||
|
|
@ -200,22 +190,20 @@ def test_dataclass_struct_anonymus_fields_1() -> None:
|
||||||
|
|
||||||
|
|
||||||
def test_dataclass_struct_anonymus_fields_2() -> None:
|
def test_dataclass_struct_anonymus_fields_2() -> None:
|
||||||
@dataclasses.dataclass
|
class TestContainer(DataclassStruct):
|
||||||
class TestContainer(DataclassMixin):
|
|
||||||
_1: 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)
|
d = constr(TestContainer)
|
||||||
assert d.build(TestContainer()) == d.build(TestContainer())
|
assert d.build(TestContainer()) == d.build(TestContainer())
|
||||||
|
|
||||||
|
|
||||||
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 TestContainer(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,7 +219,7 @@ 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)
|
d = constr(TestContainer)
|
||||||
obj = d.parse(
|
obj = d.parse(
|
||||||
d.build(
|
d.build(
|
||||||
TestContainer(
|
TestContainer(
|
||||||
|
|
@ -268,44 +256,22 @@ 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(DataclassStruct(TestContainer1).build, TestContainer2(a=1, b=2))
|
raises(constr(TestContainer1).build, TestContainer2(a=1, b=2)) == TypeError
|
||||||
== TypeError
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_dataclass_struct_doc() -> None:
|
def test_dataclass_struct_doc() -> None:
|
||||||
@dataclasses.dataclass
|
class TestContainer(DataclassStruct):
|
||||||
class TestContainer(DataclassMixin):
|
|
||||||
a: int = csfield(cs.Int16ub, "This is the documentation of a")
|
a: int = csfield(cs.Int16ub, "This is the documentation of a")
|
||||||
b: int = csfield(
|
b: int = csfield(
|
||||||
cs.Int8ub, doc="This is the documentation of b\nwhich is multiline"
|
cs.Int8ub, doc="This is the documentation of b\nwhich is multiline"
|
||||||
|
|
@ -318,7 +284,7 @@ def test_dataclass_struct_doc() -> None:
|
||||||
""",
|
""",
|
||||||
)
|
)
|
||||||
|
|
||||||
format = DataclassStruct(TestContainer)
|
format = TestContainer.__constr__()
|
||||||
common(format, b"\x00\x01\x02\x03", TestContainer(a=1, b=2, c=3), 4)
|
common(format, b"\x00\x01\x02\x03", TestContainer(a=1, b=2, c=3), 4)
|
||||||
|
|
||||||
assert format.subcon.a.docs == "This is the documentation of a"
|
assert format.subcon.a.docs == "This is the documentation of a"
|
||||||
|
|
@ -330,39 +296,36 @@ def test_dataclass_struct_doc() -> None:
|
||||||
|
|
||||||
|
|
||||||
def test_dataclass_bitstruct() -> None:
|
def test_dataclass_bitstruct() -> None:
|
||||||
@dataclasses.dataclass
|
class TestContainer(DataclassBitStruct):
|
||||||
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(TestContainer),
|
||||||
b"\xFD\x12",
|
b"\xFD\x12",
|
||||||
TestContainer(a=0x7E, b=1, c=0x12),
|
TestContainer(a=0x7E, b=1, c=0x12),
|
||||||
2,
|
2,
|
||||||
)
|
)
|
||||||
|
|
||||||
# check __getattr__
|
# check __getattr__
|
||||||
c = DataclassStruct(TestContainer)
|
c = TestContainer.__constr__()
|
||||||
assert c.a.name == "a"
|
assert c.subcon.a.name == "a"
|
||||||
assert c.b.name == "b"
|
assert c.subcon.b.name == "b"
|
||||||
assert c.c.name == "c"
|
assert c.subcon.c.name == "c"
|
||||||
assert isinstance(c.a.subcon, cs.BitsInteger)
|
assert isinstance(c.subcon.a.subcon, cs.BitsInteger)
|
||||||
assert c.b.subcon is cs.Bit
|
assert c.subcon.b.subcon is cs.Bit
|
||||||
assert isinstance(c.c.subcon, cs.BitsInteger)
|
assert isinstance(c.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)
|
d = constr(TestEnum)
|
||||||
|
|
||||||
common(d, b"\x01", TestEnum.one, 1)
|
common(d, b"\x01", TestEnum.one, 1)
|
||||||
common(d, b"\xff", TestEnum(255), 1)
|
common(d, b"\xff", TestEnum(255), 1)
|
||||||
|
|
@ -375,62 +338,40 @@ def test_tenum() -> None:
|
||||||
assert raises(d.build, 8) == TypeError
|
assert raises(d.build, 8) == TypeError
|
||||||
|
|
||||||
|
|
||||||
def test_tenum_no_enumbase() -> None:
|
def test_tenum_in_dataclass_struct() -> None:
|
||||||
class E(enum.Enum):
|
class TestEnum(TEnum, subcon=cs.Int8ub):
|
||||||
a = 1
|
a = 1
|
||||||
b = 2
|
b = 2
|
||||||
|
|
||||||
cls = t.cast(t.Type[cst.EnumBase], E)
|
class TestContainer(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(TestContainer),
|
||||||
b"\x01\x02",
|
b"\x01\x02",
|
||||||
TestContainer(a=TestEnum.a, b=2),
|
TestContainer(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, TestContainer(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)
|
d = constr(TestFlags)
|
||||||
common(d, b"\x03", TestEnum.one | TestEnum.two, 1)
|
common(d, b"\x03", TestFlags.one | TestFlags.two, 1)
|
||||||
assert d.build(TestEnum(0)) == b"\x00"
|
assert d.build(TestFlags(0)) == b"\x00"
|
||||||
assert d.build(TestEnum.one | TestEnum.two) == b"\x03"
|
assert d.build(TestFlags.one | TestFlags.two) == b"\x03"
|
||||||
assert d.build(TestEnum(8)) == b"\x08"
|
assert d.build(TestFlags(8)) == b"\x08"
|
||||||
assert d.build(TestEnum(1 | 2)) == b"\x03"
|
assert d.build(TestFlags(1 | 2)) == b"\x03"
|
||||||
assert d.build(TestEnum(255)) == b"\xff"
|
assert d.build(TestFlags(255)) == b"\xff"
|
||||||
assert d.build(TestEnum.eight) == b"\x08"
|
assert d.build(TestFlags.eight) == b"\x08"
|
||||||
assert raises(d.build, 2) == TypeError
|
assert raises(d.build, 2) == TypeError
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue