Compare commits

...
Sign in to create a new pull request.

25 commits

Author SHA1 Message Date
Tim Rid
e730fd8d87 use a special MISSING enum, because typing.Literal only accepts enums 2022-02-20 21:14:05 +01:00
Tim Rid
af1a7799e9 make consistent namings in tests 2022-02-20 20:42:36 +01:00
Tim Rid
c5d08f9b87 add docstring to construct docs 2022-02-20 20:33:28 +01:00
Tim Rid
acc9f6a9e4 removed this_struct and added a lambda instead 2022-02-20 17:07:41 +01:00
Tim Rid
935f021a4f added a litte bit of documentation 2022-02-20 16:53:12 +01:00
Tim Rid
a46c180e7a Dont parse the provided subcon in csfield. Only check if it builds from none. But now you can use the const or default parameters instead to provied default or constant values. 2022-02-20 12:47:10 +01:00
Tim Rid
cab9a07c4f Use standard dataclasses modul for python >= 3.10 or use the provided dataclasses module from this library fol python < 3.10. So we can use the kw_only option. 2022-02-20 12:32:49 +01:00
Tim Rid
8529bd2878 ignore typing issues and removed unnessesary imports 2022-02-19 21:56:50 +01:00
Tim Rid
46fc3dd48c renamed file 2022-02-19 21:55:31 +01:00
Tim Rid
f9e2d24d71 Use marker instances from the original dataclass module so that hopefully also the original dataclass functions are working 2022-02-19 21:55:16 +01:00
Tim Rid
a0cadcce85 removed things that not work in Python 3.8 2022-02-19 21:54:04 +01:00
Tim Rid
b21c43ca65 added dataclass module from python 3.10 2022-02-19 21:27:52 +01:00
Tim Rid
75dbd8d822 fixed some mypy issues 2022-02-19 14:10:18 +01:00
Tim Rid
be5ae240fb renamed construct to constr so that there are no naming collisions with the construct package name 2022-02-19 12:46:28 +01:00
Tim Rid
94a8097bec added this_struct to public interface 2022-02-13 19:48:00 +01:00
Tim Rid
e99dd5d752 some renaming 2022-02-13 19:30:35 +01:00
Tim Rid
8525c04165 added custom _EnumMeta, because __init_subclass__ is not working correctly together with the standard enum.EnumMeta... 2022-02-13 19:29:56 +01:00
Tim Rid
7c68aeecd4 adapted all tests to the new api 2022-02-13 19:01:10 +01:00
Tim Rid
96ef565044 Merge branch 'main' into feature/new-structure 2022-02-13 17:03:38 +01:00
Tim Rid
de76415ac5 changed vscode test configuration to use the native testing api instead of the Test-Explorer 2022-02-13 17:02:46 +01:00
Tim Rid
9a9b4ab95a fixed type error 2022-02-13 17:00:28 +01:00
Tim Rid
8169f0ed31 Changed implementation of TEnum and TEnumFlags. 2022-02-13 16:58:54 +01:00
Tim Rid
b896457f90 Changed implementation of DataclassStruct. It is now only nessesary to sublcass DataclassStruct and not to combine it with @dataclasses.dataclass. Also now the DataclassConstruct is included in the DataclassStruct class type itself. 2022-02-13 16:32:30 +01:00
Tim Rid
753e4282ee renamed generic_wrapper.py to generic.py and removed relative imports 2022-02-13 16:13:11 +01:00
Tim Rid
db9b35a0c2 added Constructable Protocol 2022-02-13 16:04:27 +01:00
10 changed files with 2115 additions and 448 deletions

6
.vscode/launch.json vendored
View file

@ -9,12 +9,6 @@
"type": "python",
"request": "launch",
"program": "${file}",
"console": "integratedTerminal"
},
{
"name": "Debug Tests",
"type": "python",
"request": "test",
"console": "integratedTerminal",
"justMyCode": false
}

21
.vscode/settings.json vendored
View file

@ -1,24 +1,25 @@
{
"python.pythonPath": "python",
"python.languageServer": "Pylance",
// "python.testing.unittestEnabled": false,
// "python.testing.nosetestsEnabled": false,
// "python.testing.pytestEnabled": true,
"pythonTestExplorer.testFramework": "pytest",
// configure code formating
"python.formatting.provider": "black",
"python.sortImports.path": "isort",
"python.sortImports.args": [
"--profile=black",
],
// "[python]": {
// "editor.codeActionsOnSave": {
// "source.organizeImports": true
// }
// }
// configure pylance
"python.analysis.typeCheckingMode": "strict",
"python.analysis.autoImportCompletions": false,
"python.analysis.diagnosticSeverityOverrides": {
"reportPrivateUsage": "information",
"reportUntypedNamedTuple": "information",
},
// configure pytest
"python.testing.pytestArgs": [
"tests"
],
"python.testing.unittestEnabled": false,
"python.testing.pytestEnabled": true,
}

View file

@ -1,16 +1,10 @@
from .dataclass_struct import (
from construct_typed.generic import constr
from construct_typed.dataclass_struct import (
DataclassBitStruct,
DataclassMixin,
DataclassStruct,
TBitStruct,
TContainerBase,
TContainerMixin,
TStruct,
TStructField,
csfield,
sfield,
csfield
)
from .generic_wrapper import (
from construct_typed.generic import (
Adapter,
ConstantOrContextLambda,
Construct,
@ -18,23 +12,17 @@ from .generic_wrapper import (
ListContainer,
PathType,
)
from .tenum import EnumBase, FlagsEnumBase, TEnum, TFlagsEnum
from construct_typed.tenum import TEnum, TFlags, TEnumConstruct, TFlagsConstruct
__all__ = [
"DataclassBitStruct",
"DataclassMixin",
"DataclassStruct",
"TBitStruct",
"TContainerBase",
"TContainerMixin",
"TStruct",
"TStructField",
"constr",
"csfield",
"sfield",
"EnumBase",
"FlagsEnumBase",
"TEnum",
"TFlagsEnum",
"TEnumConstruct",
"TFlags",
"TFlagsConstruct",
"Adapter",
"ConstantOrContextLambda",
"Construct",

View file

@ -1,8 +1,10 @@
# -*- coding: utf-8 -*-
# pyright: strict
import dataclasses
import sys
import textwrap
import typing as t
import enum
import construct as cs
from construct.lib.containers import (
@ -12,21 +14,290 @@ from construct.lib.containers import (
)
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'
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),
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.
This method also processes Const and Default, to pass these values als default values to the dataclass.
Only one of the parameters `default` or `const` can be vaild. They are mutually exclusive.
"""
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:
return getattr(self, key)
@ -72,201 +343,43 @@ class DataclassMixin:
text.append(indentation.join(str(v).split("\n")))
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:
def __new__(
cls,
dc_type: t.Type[DataclassType],
reverse: bool = False,
) -> "DataclassStruct[DataclassType]":
@classmethod
def __constr__(cls: t.Type[T]) -> "DataclassConstruct[T]":
...
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
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]",
]:
class DataclassBitStruct(DataclassStruct):
r"""
Makes a DataclassStruct inside a Bitwise.
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 reverse: Flag if the fields of the dataclass should be reversed
:param constr: TODO
:param reverse_fields: Flag if the fields of the dataclass should be reversed
Example::
DataclassBitStruct <--> Bitwise(DataclassStruct(...))
>>> import dataclasses
TODO:
>>> from construct import BitsInteger, Flag, Nibble, Padding
>>> from construct_typed import DataclassBitStruct, DataclassMixin, csfield
>>> @dataclasses.dataclass
... class TestDataclass(DataclassMixin):
>>> from construct_typed import DataclassBitStruct, csfield, construct
... class TestDataclass(DataclassBitStruct):
... a: int = csfield(Flag)
... b: int = csfield(Nibble)
... c: int = csfield(BitsInteger(10))
... d: None = csfield(Padding(1))
>>> d = DataclassBitStruct(TestDataclass)
>>> d = construct(TestDataclass)
>>> d.parse(b"\x01\x02")
TestDataclass(a=False, b=0, c=129, d=None)
"""
return cs.Bitwise(DataclassStruct(dc_type, reverse))
# support legacy names
TStruct = DataclassStruct
TBitStruct = DataclassBitStruct
TContainerMixin = DataclassMixin
TContainerBase = DataclassMixin
TStructField = csfield
sfield = csfield
@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:
DataclassStruct.__init_subclass__.__func__(cls, lambda cls: cs.Bitwise(constr(cls)), reverse_fields) # type: ignore

File diff suppressed because it is too large Load diff

View file

@ -39,3 +39,20 @@ else:
ConstantOrContextLambda = t.Union[ValueType, t.Callable[[Context], t.Any]]
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

View file

@ -1,28 +1,106 @@
import enum
import textwrap
import typing as t
from .generic_wrapper import *
import construct as cs
from construct_typed.generic import *
T = t.TypeVar("T")
# ## TEnum ############################################################################################################
class EnumBase(enum.IntEnum):
class _EnumMeta(enum.EnumMeta):
@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.
"""
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
# not found in the enum, a new pseudo member is created.
# The idea is taken from: https://stackoverflow.com/a/57179436
@classmethod
def _missing_(cls, value: t.Any) -> t.Optional["EnumBase"]:
def _missing_(cls, value: t.Any) -> t.Optional["TEnum"]:
if isinstance(value, int):
return cls._create_pseudo_member_(value)
return None # will raise the ValueError in Enum.__new__
@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
if pseudo_member is None:
new_member = int.__new__(cls, value)
@ -34,10 +112,10 @@ class EnumBase(enum.IntEnum):
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.
"""
@ -46,20 +124,20 @@ class TEnum(Adapter[int, int, EnumType, EnumType]):
def __new__(
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]):
if not issubclass(enum_type, EnumBase):
if not issubclass(enum_type, TEnum):
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
self.enum_type = t.cast(t.Type[EnumType], enum_type) # type: ignore
# 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:
return self.enum_type(obj)
@ -77,44 +155,60 @@ class TEnum(Adapter[int, int, EnumType, EnumType]):
)
# ## TFlagsEnum #######################################################################################################
class FlagsEnumBase(enum.IntFlag):
pass
# ## TFlags #######################################################################################################
class TFlags(enum.IntFlag, metaclass=_EnumMeta):
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:
def __new__(
cls, subcon: Construct[int, int], enum_type: t.Type[FlagsEnumType]
) -> "TFlagsEnum[FlagsEnumType]":
cls, subcon: Construct[int, int], enum_type: t.Type[FlagsType]
) -> "TFlagsConstruct[FlagsType]":
...
def __init__(self, subcon: Construct[int, int], enum_type: t.Type[FlagsEnumType]):
if not issubclass(enum_type, FlagsEnumBase):
def __init__(self, subcon: Construct[int, int], enum_type: t.Type[FlagsType]):
if not issubclass(enum_type, TFlags):
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
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
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)
def _encode(
self,
obj: FlagsEnumType,
obj: FlagsType,
context: Context,
path: PathType,
) -> int:

View file

@ -1,2 +1,2 @@
version = (0, 5, 2)
version_string = "0.5.2"
version_string = "0.5.2"

View file

@ -6,7 +6,7 @@ import construct_typed as cst
Buffer = t.Union[bytes, memoryview, bytearray]
ParsedType = t.TypeVar("ParsedType")
BuildTypes = t.TypeVar("BuildTypes")
ContainerType = t.TypeVar("ContainerType", bound=cst.TContainerMixin)
ContainerType = t.TypeVar("ContainerType", bound=cst.DataclassStruct)
T = t.TypeVar("T")
IdentType = t.TypeVar("IdentType")
@ -20,7 +20,7 @@ def raises(
) -> t.Union[t.Any, Exception]: ...
@t.overload
def common(
format: cst.TStruct[ContainerType],
format: ContainerType,
datasample: Buffer,
objsample: t.Union[ContainerType, t.Dict[str, t.Any]],
sizesample: t.Union[int, t.Type[Exception]] = ...,

View file

@ -1,72 +1,80 @@
# -*- coding: utf-8 -*-
# pyright: strict
import dataclasses
import enum
import typing as t
import construct as cs
import construct_typed as cst
from construct_typed import DataclassBitStruct, DataclassMixin, DataclassStruct, csfield
from construct_typed import (
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:
@dataclasses.dataclass
class ConstDefaultTest(DataclassMixin):
const_bytes: bytes = csfield(cs.Const(b"BMP"))
const_int: int = csfield(cs.Const(5, cs.Int8ub))
default_int: int = csfield(cs.Default(cs.Int8ub, 28))
default_lambda: bytes = csfield(
class TestDataclass(DataclassStruct):
const_bytes: bytes = csfield(cs.Bytes(3), const=b"BMP")
const_int: int = csfield(cs.Int8ub, const=5)
default_int: int = csfield(cs.Int8ub, default=26)
default_lambda: t.Optional[bytes] = csfield(
cs.Default(cs.Bytes(cs.this.const_int), lambda ctx: bytes(ctx.const_int))
)
a = ConstDefaultTest()
assert a.const_bytes == b"BMP"
assert a.const_int == 5
assert a.default_int == 28
assert a.default_lambda == None
obj = TestDataclass()
assert obj.const_bytes == b"BMP"
assert obj.const_int == 5
assert obj.default_int == 26
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:
@dataclasses.dataclass
class TestTContainer(DataclassMixin):
a: t.Optional[int] = csfield(cs.Const(1, cs.Byte))
class TestDataclass(DataclassStruct):
a: int = csfield(cs.Byte, const=1)
b: int = csfield(cs.Int8ub)
tcontainer = TestTContainer(b=2)
obj = TestDataclass(b=2)
# tcontainer
assert tcontainer.a == 1
assert tcontainer["a"] == 1
assert tcontainer.b == 2
assert tcontainer["b"] == 2
assert obj.a == 1
assert obj["a"] == 1
assert obj.b == 2
assert obj["b"] == 2
tcontainer.a = 5
assert tcontainer.a == 5
assert tcontainer["a"] == 5
tcontainer["a"] = 6
assert tcontainer.a == 6
assert tcontainer["a"] == 6
obj.a = 5
assert obj.a == 5
assert obj["a"] == 5
obj["a"] = 6
assert obj.a == 6
assert obj["a"] == 6
# 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:
@dataclasses.dataclass
class Image(DataclassMixin):
signature: t.Optional[bytes] = csfield(cs.Const(b"BMP"))
class Image(DataclassStruct):
signature: bytes = csfield(cs.Bytes(3), const=b"BMP")
width: int = csfield(cs.Int8ub)
height: int = csfield(cs.Int8ub)
format = DataclassStruct(Image)
fmt = constr(Image)
obj = Image(width=3, height=2)
assert (
str(obj)
== "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 (
str(obj)
== "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:
@dataclasses.dataclass
class Image(DataclassMixin):
class Image(DataclassStruct):
width: int = csfield(cs.Int8ub)
height: int = csfield(cs.Int8ub)
pixels: bytes = csfield(cs.Bytes(cs.this.height * cs.this.width))
common(
cst.DataclassStruct(Image),
constr(Image),
b"\x01\x0212",
Image(width=1, height=2, pixels=b"12"),
)
# check __getattr__
c = cst.DataclassStruct(Image)
assert c.width.name == "width"
assert c.height.name == "height"
assert c.width.subcon is cs.Int8ub
assert c.height.subcon is cs.Int8ub
fmt = Image.__constr__()
assert fmt.width.name == "width"
assert fmt.height.name == "height"
assert fmt.width.subcon is cs.Int8ub
assert fmt.height.subcon is cs.Int8ub
def test_dataclass_struct_reverse() -> None:
@dataclasses.dataclass
class TestContainer(DataclassMixin):
class TestDataclass(DataclassStruct, reverse_fields=True):
a: int = csfield(cs.Int16ub)
b: int = csfield(cs.Int8ub)
common(
DataclassStruct(TestContainer, reverse=True),
constr(TestDataclass),
b"\x02\x00\x01",
TestContainer(a=1, b=2),
TestDataclass(a=1, b=2),
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:
@dataclasses.dataclass
class TestContainer(DataclassMixin):
@dataclasses.dataclass
class InnerDataclass(DataclassMixin):
class TestDataclass(DataclassStruct):
class InnerDataclass(DataclassStruct):
b: int = csfield(cs.Byte)
c: bytes = csfield(cs.Bytes(cs.this._.length))
length: int = csfield(cs.Byte)
a: InnerDataclass = csfield(DataclassStruct(InnerDataclass))
a: InnerDataclass = csfield(constr(InnerDataclass))
common(
DataclassStruct(TestContainer),
constr(TestDataclass),
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:
@dataclasses.dataclass
class Image(DataclassMixin):
class Image(DataclassStruct):
width: int = csfield(cs.Int8ub)
height: int = csfield(cs.Int8ub)
pixels: t.Optional[bytes] = csfield(
@ -142,80 +142,75 @@ def test_dataclass_struct_default_field() -> None:
)
common(
DataclassStruct(Image),
constr(Image),
b"\x02\x03\x00\x00\x00\x00\x00\x00",
setattrs(Image(2, 3), pixels=bytes(6)),
sample_building=Image(2, 3),
setattrs(Image(width=2, height=3), pixels=bytes(6)),
sample_building=Image(width=2, height=3),
)
def test_dataclass_struct_const_field() -> None:
@dataclasses.dataclass
class TestContainer(DataclassMixin):
class TestDataclass(DataclassStruct):
const_field: t.Optional[bytes] = csfield(cs.Const(b"\x00"))
common(
DataclassStruct(TestContainer),
constr(TestDataclass),
bytes(1),
setattrs(TestContainer(), const_field=b"\x00"),
setattrs(TestDataclass(), const_field=b"\x00"),
1,
)
assert (
raises(
DataclassStruct(TestContainer).build,
setattrs(TestContainer(), const_field=b"\x01"),
constr(TestDataclass).build,
setattrs(TestDataclass(), const_field=b"\x01"),
)
== cs.ConstError
)
def test_dataclass_struct_array_field() -> None:
@dataclasses.dataclass
class TestContainer(DataclassMixin):
class TestDataclass(DataclassStruct):
array_field: t.List[int] = csfield(cs.Array(5, cs.Int8ub))
common(
DataclassStruct(TestContainer),
constr(TestDataclass),
bytes(5),
TestContainer(array_field=[0, 0, 0, 0, 0]),
TestDataclass(array_field=[0, 0, 0, 0, 0]),
5,
)
def test_dataclass_struct_anonymus_fields_1() -> None:
@dataclasses.dataclass
class TestContainer(DataclassMixin):
class TestDataclass(DataclassStruct):
_1: t.Optional[bytes] = csfield(cs.Const(b"\x00"))
_2: None = csfield(cs.Padding(1))
_3: None = csfield(cs.Pass)
_4: None = csfield(cs.Terminated)
common(
DataclassStruct(TestContainer),
constr(TestDataclass),
bytes(2),
setattrs(TestContainer(), _1=b"\x00"),
setattrs(TestDataclass(), _1=b"\x00"),
cs.SizeofError,
)
def test_dataclass_struct_anonymus_fields_2() -> None:
@dataclasses.dataclass
class TestContainer(DataclassMixin):
_1: int = csfield(cs.Computed(7))
class TestDataclass(DataclassStruct):
_1: t.Optional[int] = csfield(cs.Computed(7))
_2: t.Optional[bytes] = csfield(cs.Const(b"JPEG"))
_3: None = csfield(cs.Pass)
_4: None = csfield(cs.Terminated)
d = DataclassStruct(TestContainer)
assert d.build(TestContainer()) == d.build(TestContainer())
fmt = constr(TestDataclass)
assert fmt.build(TestDataclass()) == fmt.build(TestDataclass())
def test_dataclass_struct_overloaded_method() -> None:
# Test dot access to some names that are not accessable via dot
# in the original 'cs.Container'.
@dataclasses.dataclass
class TestContainer(DataclassMixin):
class TestDataclass(DataclassStruct):
clear: int = csfield(cs.Int8ul)
copy: 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)
values: int = csfield(cs.Int8ul)
d = DataclassStruct(TestContainer)
obj = d.parse(
d.build(
TestContainer(
fmt = constr(TestDataclass)
obj = fmt.parse(
fmt.build(
TestDataclass(
clear=1,
copy=2,
fromkeys=3,
@ -268,169 +263,172 @@ def test_dataclass_struct_overloaded_method() -> None:
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:
@dataclasses.dataclass
class TestContainer1(DataclassMixin):
class TestContainer1(DataclassStruct):
a: int = csfield(cs.Int16ub)
b: int = csfield(cs.Int8ub)
@dataclasses.dataclass
class TestContainer2(DataclassMixin):
class TestContainer2(DataclassStruct):
a: int = csfield(cs.Int16ub)
b: int = csfield(cs.Int8ub)
assert (
raises(DataclassStruct(TestContainer1).build, TestContainer2(a=1, b=2))
== TypeError
)
assert raises(constr(TestContainer1).build, TestContainer2(a=1, b=2)) == TypeError
def test_dataclass_struct_doc() -> None:
@dataclasses.dataclass
class TestContainer(DataclassMixin):
a: int = csfield(cs.Int16ub, "This is the documentation of a")
b: int = csfield(
cs.Int8ub, doc="This is the documentation of b\nwhich is multiline"
)
class TestDataclass1(DataclassStruct):
"""
Documentation of TestDataclass1
"""
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(
cs.Int8ub,
"""
This is the documentation of c
doc="""
This is the doc of c
which is also multiline
""",
)
format = DataclassStruct(TestContainer)
common(format, b"\x00\x01\x02\x03", TestContainer(a=1, b=2, c=3), 4)
fmt1 = TestDataclass1.__constr__()
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 format.subcon.b.docs == "This is the documentation of b\nwhich is multiline"
assert (
format.subcon.c.docs
== "This is the documentation of c\nwhich is also multiline"
)
assert fmt1.docs == "Documentation of TestDataclass1"
assert fmt1.subcon.a.docs == "This is the doc of a"
assert fmt1.subcon.b.docs == "This is the doc of b\nwhich is multiline"
assert fmt1.subcon.c.docs == "This is the doc 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:
@dataclasses.dataclass
class TestContainer(DataclassMixin):
def test_dataclass_bitwise() -> None:
class TestDataclass(DataclassStruct, constr=lambda cls: cs.Bitwise(cls)):
a: int = csfield(cs.BitsInteger(7))
b: int = csfield(cs.Bit)
c: int = csfield(cs.BitsInteger(8))
print("")
common(
DataclassBitStruct(TestContainer),
constr(TestDataclass),
b"\xFD\x12",
TestContainer(a=0x7E, b=1, c=0x12),
TestDataclass(a=0x7E, b=1, c=0x12),
2,
)
# check __getattr__
c = DataclassStruct(TestContainer)
assert c.a.name == "a"
assert c.b.name == "b"
assert c.c.name == "c"
assert isinstance(c.a.subcon, cs.BitsInteger)
assert c.b.subcon is cs.Bit
assert isinstance(c.c.subcon, cs.BitsInteger)
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_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:
class TestEnum(cst.EnumBase):
class TestEnum(TEnum, subcon=cs.Byte):
one = 1
two = 2
four = 4
eight = 8
d = cst.TEnum(cs.Byte, TestEnum)
fmt = constr(TestEnum)
common(d, b"\x01", TestEnum.one, 1)
common(d, b"\xff", TestEnum(255), 1)
assert d.parse(b"\x01") == TestEnum.one
assert d.parse(b"\x01") == 1
assert int(d.parse(b"\x01")) == 1
assert d.parse(b"\xff") == TestEnum(255)
assert d.parse(b"\xff") == 255
assert int(d.parse(b"\xff")) == 255
assert raises(d.build, 8) == TypeError
common(fmt, b"\x01", TestEnum.one, 1)
common(fmt, b"\xff", TestEnum(255), 1)
assert fmt.parse(b"\x01") == TestEnum.one
assert fmt.parse(b"\x01") == 1
assert int(fmt.parse(b"\x01")) == 1
assert fmt.parse(b"\xff") == TestEnum(255)
assert fmt.parse(b"\xff") == 255
assert int(fmt.parse(b"\xff")) == 255
assert raises(fmt.build, 8) == TypeError
def test_tenum_no_enumbase() -> None:
class E(enum.Enum):
def test_tenum_doc() -> None:
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
b = 2
cls = t.cast(t.Type[cst.EnumBase], E)
assert raises(lambda: cst.TEnum(cs.Byte, cls)) == TypeError
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))
class TestDataclass(DataclassStruct):
a: TestEnum = csfield(constr(TestEnum))
b: int = csfield(cs.Int8ub)
common(
DataclassStruct(TestContainer),
constr(TestDataclass),
b"\x01\x02",
TestContainer(a=TestEnum.a, b=2),
TestDataclass(a=TestEnum.a, b=2),
2,
)
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:
class TestEnum(cst.FlagsEnumBase):
def test_tflags() -> None:
class TestFlags(TFlags, subcon=cs.Byte):
one = 1
two = 2
four = 4
eight = 8
d = cst.TFlagsEnum(cs.Byte, TestEnum)
common(d, b"\x03", TestEnum.one | TestEnum.two, 1)
assert d.build(TestEnum(0)) == b"\x00"
assert d.build(TestEnum.one | TestEnum.two) == b"\x03"
assert d.build(TestEnum(8)) == b"\x08"
assert d.build(TestEnum(1 | 2)) == b"\x03"
assert d.build(TestEnum(255)) == b"\xff"
assert d.build(TestEnum.eight) == b"\x08"
assert raises(d.build, 2) == TypeError
fmt = constr(TestFlags)
common(fmt, b"\x03", TestFlags.one | TestFlags.two, 1)
assert fmt.build(TestFlags(0)) == b"\x00"
assert fmt.build(TestFlags.one | TestFlags.two) == b"\x03"
assert fmt.build(TestFlags(8)) == b"\x08"
assert fmt.build(TestFlags(1 | 2)) == b"\x03"
assert fmt.build(TestFlags(255)) == b"\xff"
assert fmt.build(TestFlags.eight) == b"\x08"
assert raises(fmt.build, 2) == TypeError