diff --git a/.vscode/launch.json b/.vscode/launch.json index d94289d..8655026 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -9,12 +9,6 @@ "type": "python", "request": "launch", "program": "${file}", - "console": "integratedTerminal" - }, - { - "name": "Debug Tests", - "type": "python", - "request": "test", "console": "integratedTerminal", "justMyCode": false } diff --git a/.vscode/settings.json b/.vscode/settings.json index 76bdfe9..b265945 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -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, } \ No newline at end of file diff --git a/construct_typed/__init__.py b/construct_typed/__init__.py index 00d5093..56c0e57 100644 --- a/construct_typed/__init__.py +++ b/construct_typed/__init__.py @@ -1,16 +1,11 @@ -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, + this_struct ) -from .generic_wrapper import ( +from construct_typed.generic import ( Adapter, ConstantOrContextLambda, Construct, @@ -18,23 +13,18 @@ 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", + "this_struct", "Adapter", "ConstantOrContextLambda", "Construct", diff --git a/construct_typed/dataclass_struct.py b/construct_typed/dataclass_struct.py index 78d2e59..ea89f5a 100644 --- a/construct_typed/dataclass_struct.py +++ b/construct_typed/dataclass_struct.py @@ -12,21 +12,249 @@ 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 + +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' - 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. + """ + 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 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: return getattr(self, key) @@ -72,201 +300,41 @@ 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 \ No newline at end of file + @classmethod + def __init_subclass__( + cls, + constr: "cs.Construct[t.Any, t.Any]" = this_struct, + reverse_fields: bool = False, + ) -> None: + cls = DataclassStruct.__init_subclass__.__func__(cls, cs.Bitwise(constr), reverse_fields) # type: ignore diff --git a/construct_typed/generic_wrapper.py b/construct_typed/generic.py similarity index 72% rename from construct_typed/generic_wrapper.py rename to construct_typed/generic.py index aa4af4c..b988444 100644 --- a/construct_typed/generic_wrapper.py +++ b/construct_typed/generic.py @@ -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 diff --git a/construct_typed/tenum.py b/construct_typed/tenum.py index 7c93e33..83f1d8c 100644 --- a/construct_typed/tenum.py +++ b/construct_typed/tenum.py @@ -1,28 +1,97 @@ import enum import typing as t -from .generic_wrapper import * +import construct as cs + +from .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: + # 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. """ + 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 +103,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 +115,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 +146,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: diff --git a/construct_typed/version.py b/construct_typed/version.py index 1a555cf..9498baa 100644 --- a/construct_typed/version.py +++ b/construct_typed/version.py @@ -1,2 +1,2 @@ version = (0, 5, 2) -version_string = "0.5.2" \ No newline at end of file +version_string = "0.5.2" diff --git a/tests/declarativeunittest.pyi b/tests/declarativeunittest.pyi index e2f8cab..5afd044 100644 --- a/tests/declarativeunittest.pyi +++ b/tests/declarativeunittest.pyi @@ -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]] = ..., diff --git a/tests/test_typed.py b/tests/test_typed.py index d74e025..ef0226b 100644 --- a/tests/test_typed.py +++ b/tests/test_typed.py @@ -1,19 +1,22 @@ # -*- 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 def test_dataclass_const_default() -> None: - @dataclasses.dataclass - class ConstDefaultTest(DataclassMixin): + class ConstDefaultTest(DataclassStruct): 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)) @@ -29,8 +32,7 @@ def test_dataclass_const_default() -> None: def test_dataclass_access() -> None: - @dataclasses.dataclass - class TestTContainer(DataclassMixin): + class TestTContainer(DataclassStruct): a: t.Optional[int] = csfield(cs.Const(1, cs.Byte)) b: int = csfield(cs.Int8ub) @@ -50,17 +52,16 @@ def test_dataclass_access() -> None: assert tcontainer["a"] == 6 # 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: - @dataclasses.dataclass - class Image(DataclassMixin): + class Image(DataclassStruct): signature: t.Optional[bytes] = csfield(cs.Const(b"BMP")) width: int = csfield(cs.Int8ub) height: int = csfield(cs.Int8ub) - format = DataclassStruct(Image) + format = constr(Image) obj = Image(width=3, height=2) assert ( str(obj) @@ -74,20 +75,19 @@ 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) + c = Image.__constr__() assert c.width.name == "width" assert c.height.name == "height" assert c.width.subcon is cs.Int8ub @@ -95,43 +95,36 @@ def test_dataclass_struct() -> None: def test_dataclass_struct_reverse() -> None: - @dataclasses.dataclass - class TestContainer(DataclassMixin): + class TestContainer(DataclassStruct, reverse_fields=True): a: int = csfield(cs.Int16ub) b: int = csfield(cs.Int8ub) common( - DataclassStruct(TestContainer, reverse=True), + constr(TestContainer), b"\x02\x00\x01", TestContainer(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 TestContainer(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(TestContainer), b"\x02\x01\xF1\xF2", TestContainer(length=2, a=TestContainer.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,7 +135,7 @@ 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), @@ -150,12 +143,11 @@ def test_dataclass_struct_default_field() -> None: def test_dataclass_struct_const_field() -> None: - @dataclasses.dataclass - class TestContainer(DataclassMixin): + class TestContainer(DataclassStruct): const_field: t.Optional[bytes] = csfield(cs.Const(b"\x00")) common( - DataclassStruct(TestContainer), + constr(TestContainer), bytes(1), setattrs(TestContainer(), const_field=b"\x00"), 1, @@ -163,7 +155,7 @@ def test_dataclass_struct_const_field() -> None: assert ( raises( - DataclassStruct(TestContainer).build, + constr(TestContainer).build, setattrs(TestContainer(), const_field=b"\x01"), ) == cs.ConstError @@ -171,12 +163,11 @@ def test_dataclass_struct_const_field() -> None: def test_dataclass_struct_array_field() -> None: - @dataclasses.dataclass - class TestContainer(DataclassMixin): + class TestContainer(DataclassStruct): array_field: t.List[int] = csfield(cs.Array(5, cs.Int8ub)) common( - DataclassStruct(TestContainer), + constr(TestContainer), bytes(5), TestContainer(array_field=[0, 0, 0, 0, 0]), 5, @@ -184,15 +175,14 @@ def test_dataclass_struct_array_field() -> None: def test_dataclass_struct_anonymus_fields_1() -> None: - @dataclasses.dataclass - class TestContainer(DataclassMixin): + class TestContainer(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(TestContainer), bytes(2), setattrs(TestContainer(), _1=b"\x00"), cs.SizeofError, @@ -200,22 +190,20 @@ def test_dataclass_struct_anonymus_fields_1() -> None: def test_dataclass_struct_anonymus_fields_2() -> None: - @dataclasses.dataclass - class TestContainer(DataclassMixin): + class TestContainer(DataclassStruct): _1: 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) + d = constr(TestContainer) assert d.build(TestContainer()) == d.build(TestContainer()) 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 TestContainer(DataclassStruct): clear: int = csfield(cs.Int8ul) copy: 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) values: int = csfield(cs.Int8ul) - d = DataclassStruct(TestContainer) + d = constr(TestContainer) obj = d.parse( d.build( TestContainer( @@ -268,44 +256,22 @@ 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 + raises(constr(TestContainer1).build, TestContainer2(a=1, b=2)) == TypeError ) def test_dataclass_struct_doc() -> None: - @dataclasses.dataclass - class TestContainer(DataclassMixin): + class TestContainer(DataclassStruct): 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" @@ -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) 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: - @dataclasses.dataclass - class TestContainer(DataclassMixin): + class TestContainer(DataclassBitStruct): a: int = csfield(cs.BitsInteger(7)) b: int = csfield(cs.Bit) c: int = csfield(cs.BitsInteger(8)) - print("") - common( - DataclassBitStruct(TestContainer), + constr(TestContainer), b"\xFD\x12", TestContainer(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) + c = TestContainer.__constr__() + assert c.subcon.a.name == "a" + assert c.subcon.b.name == "b" + assert c.subcon.c.name == "c" + assert isinstance(c.subcon.a.subcon, cs.BitsInteger) + assert c.subcon.b.subcon is cs.Bit + assert isinstance(c.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) + d = constr(TestEnum) common(d, b"\x01", TestEnum.one, 1) common(d, b"\xff", TestEnum(255), 1) @@ -375,62 +338,40 @@ def test_tenum() -> None: assert raises(d.build, 8) == TypeError -def test_tenum_no_enumbase() -> None: - class E(enum.Enum): +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 TestContainer(DataclassStruct): + a: TestEnum = csfield(constr(TestEnum)) b: int = csfield(cs.Int8ub) common( - DataclassStruct(TestContainer), + constr(TestContainer), b"\x01\x02", TestContainer(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, TestContainer(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" + d = constr(TestFlags) + common(d, b"\x03", TestFlags.one | TestFlags.two, 1) + assert d.build(TestFlags(0)) == b"\x00" + assert d.build(TestFlags.one | TestFlags.two) == b"\x03" + assert d.build(TestFlags(8)) == b"\x08" + assert d.build(TestFlags(1 | 2)) == b"\x03" + assert d.build(TestFlags(255)) == b"\xff" + assert d.build(TestFlags.eight) == b"\x08" assert raises(d.build, 2) == TypeError