From db9b35a0c2fd3840ef9f9ac297d70e7e748a1812 Mon Sep 17 00:00:00 2001 From: Tim Rid <6593626+timrid@users.noreply.github.com> Date: Sun, 13 Feb 2022 16:04:27 +0100 Subject: [PATCH 01/12] added `Constructable` Protocol --- construct_typed/generic_wrapper.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/construct_typed/generic_wrapper.py b/construct_typed/generic_wrapper.py index aa4af4c..75c0ac2 100644 --- a/construct_typed/generic_wrapper.py +++ b/construct_typed/generic_wrapper.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 __construct__(self) -> "Construct[ParsedType, BuildTypes]": + raise NotImplementedError + + +def construct( + 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.__construct__() + return constr From 753e4282ee48259d82c0944a8a728abb33412802 Mon Sep 17 00:00:00 2001 From: Tim Rid <6593626+timrid@users.noreply.github.com> Date: Sun, 13 Feb 2022 16:13:11 +0100 Subject: [PATCH 02/12] renamed generic_wrapper.py to generic.py and removed relative imports --- construct_typed/__init__.py | 6 +++--- construct_typed/dataclass_struct.py | 4 ++-- construct_typed/{generic_wrapper.py => generic.py} | 0 construct_typed/tenum.py | 2 +- construct_typed/version.py | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) rename construct_typed/{generic_wrapper.py => generic.py} (100%) diff --git a/construct_typed/__init__.py b/construct_typed/__init__.py index 00d5093..a11e26d 100644 --- a/construct_typed/__init__.py +++ b/construct_typed/__init__.py @@ -1,4 +1,4 @@ -from .dataclass_struct import ( +from construct_typed.dataclass_struct import ( DataclassBitStruct, DataclassMixin, DataclassStruct, @@ -10,7 +10,7 @@ from .dataclass_struct import ( csfield, sfield, ) -from .generic_wrapper import ( +from construct_typed.generic import ( Adapter, ConstantOrContextLambda, Construct, @@ -18,7 +18,7 @@ from .generic_wrapper import ( ListContainer, PathType, ) -from .tenum import EnumBase, FlagsEnumBase, TEnum, TFlagsEnum +from construct_typed.tenum import EnumBase, FlagsEnumBase, TEnum, TFlagsEnum __all__ = [ "DataclassBitStruct", diff --git a/construct_typed/dataclass_struct.py b/construct_typed/dataclass_struct.py index 78d2e59..5980a82 100644 --- a/construct_typed/dataclass_struct.py +++ b/construct_typed/dataclass_struct.py @@ -12,7 +12,7 @@ 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 class DataclassMixin: @@ -269,4 +269,4 @@ TBitStruct = DataclassBitStruct TContainerMixin = DataclassMixin TContainerBase = DataclassMixin TStructField = csfield -sfield = csfield \ No newline at end of file +sfield = csfield diff --git a/construct_typed/generic_wrapper.py b/construct_typed/generic.py similarity index 100% rename from construct_typed/generic_wrapper.py rename to construct_typed/generic.py diff --git a/construct_typed/tenum.py b/construct_typed/tenum.py index 7c93e33..91c4173 100644 --- a/construct_typed/tenum.py +++ b/construct_typed/tenum.py @@ -1,7 +1,7 @@ import enum import typing as t -from .generic_wrapper import * +from construct_typed.generic import * # ## TEnum ############################################################################################################ 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" From b896457f907dd4600402e73d938cd37a9eb96fcb Mon Sep 17 00:00:00 2001 From: Tim Rid <6593626+timrid@users.noreply.github.com> Date: Sun, 13 Feb 2022 16:32:30 +0100 Subject: [PATCH 03/12] 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. --- construct_typed/dataclass_struct.py | 437 ++++++++++++++++------------ 1 file changed, 253 insertions(+), 184 deletions(-) diff --git a/construct_typed/dataclass_struct.py b/construct_typed/dataclass_struct.py index 5980a82..23a00eb 100644 --- a/construct_typed/dataclass_struct.py +++ b/construct_typed/dataclass_struct.py @@ -14,19 +14,247 @@ from construct.lib.py3compat import bytestringtype, reprstring, unicodestringtyp 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" + +if t.TYPE_CHECKING: + # 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 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={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 isinstance(dc_type, DataclassStruct): + raise TypeError(f"'{repr(dc_type)}' has to be a '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 = 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): + """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, + ): + # 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, "__construct__", lambda: constr) + + return cls + + # 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,42 @@ 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 __construct__(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, + constr: "cs.Construct[t.Any, t.Any]" = this_struct, + reverse_fields: bool = False, + ): + cls = DataclassStruct.__init_subclass__.__func__(cls, cs.Bitwise(constr), reverse_fields) # type: ignore + return cls From 8169f0ed318a497f5d9c884d153ced16e056ed9d Mon Sep 17 00:00:00 2001 From: Tim Rid <6593626+timrid@users.noreply.github.com> Date: Sun, 13 Feb 2022 16:51:20 +0100 Subject: [PATCH 04/12] Changed implementation of TEnum and TEnumFlags. --- construct_typed/__init__.py | 22 ++------ construct_typed/tenum.py | 101 ++++++++++++++++++++++++++++-------- 2 files changed, 84 insertions(+), 39 deletions(-) diff --git a/construct_typed/__init__.py b/construct_typed/__init__.py index a11e26d..10e5b12 100644 --- a/construct_typed/__init__.py +++ b/construct_typed/__init__.py @@ -1,14 +1,7 @@ from construct_typed.dataclass_struct import ( DataclassBitStruct, - DataclassMixin, DataclassStruct, - TBitStruct, - TContainerBase, - TContainerMixin, - TStruct, - TStructField, csfield, - sfield, ) from construct_typed.generic import ( Adapter, @@ -18,23 +11,16 @@ from construct_typed.generic import ( ListContainer, PathType, ) -from construct_typed.tenum import EnumBase, FlagsEnumBase, TEnum, TFlagsEnum +from construct_typed.tenum import TEnum, TFlags, TEnumConstruct, TFlagsConstruct __all__ = [ "DataclassBitStruct", - "DataclassMixin", "DataclassStruct", - "TBitStruct", - "TContainerBase", - "TContainerMixin", - "TStruct", - "TStructField", "csfield", - "sfield", - "EnumBase", - "FlagsEnumBase", "TEnum", - "TFlagsEnum", + "TEnumConstruct", + "TFlags", + "TFlagsConstruct", "Adapter", "ConstantOrContextLambda", "Construct", diff --git a/construct_typed/tenum.py b/construct_typed/tenum.py index 91c4173..4a84584 100644 --- a/construct_typed/tenum.py +++ b/construct_typed/tenum.py @@ -1,28 +1,54 @@ import enum import typing as t -from construct_typed.generic import * +import construct as cs + +from .generic import * + +T = t.TypeVar("T") # ## TEnum ############################################################################################################ -class EnumBase(enum.IntEnum): +class TEnum(enum.IntEnum): """ - 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. """ + @classmethod + def __init_subclass__( + cls, + subcon: "cs.Construct[t.Any, t.Any]", + **kwargs: t.Any, + ): + super().__init_subclass__(**kwargs) + + # validate types + if not isinstance(subcon, cs.Construct): # type: ignore + raise ValueError( + f"`subcon` parameter has to be an `Construct` object but is {type(subcon)}" + ) + + # create construct format + enum_constr = TEnumConstruct(subcon, cls) + + # save construct format and make the class compatible to `Constructable` protocol + setattr(cls, "__construct__", lambda: enum_constr) + + return cls + # 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) @@ -33,11 +59,17 @@ class EnumBase(enum.IntEnum): pseudo_member = cls._value2member_map_.setdefault(value, new_member) # type: ignore return pseudo_member # type: ignore + if t.TYPE_CHECKING: -EnumType = t.TypeVar("EnumType", bound=EnumBase) + @classmethod + def __construct__(cls: "t.Type[EnumType]") -> "TEnumConstruct[EnumType]": + ... -class TEnum(Adapter[int, int, EnumType, EnumType]): +EnumType = t.TypeVar("EnumType", bound=TEnum) + + +class TEnumConstruct(Adapter[int, int, EnumType, EnumType]): """ Typed enum. """ @@ -46,20 +78,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) @@ -76,38 +108,65 @@ class TEnum(Adapter[int, int, EnumType, EnumType]): "'{}' has to be of type {}".format(repr(obj), repr(self.enum_type)) ) +# ## TFlags ####################################################################################################### +class TFlags(enum.IntFlag): + @classmethod + def __init_subclass__( + cls, + subcon: "cs.Construct[t.Any, t.Any]", + **kwargs: t.Any, + ): + super().__init_subclass__(**kwargs) -# ## TFlagsEnum ####################################################################################################### -class FlagsEnumBase(enum.IntFlag): - pass + # validate types + if not isinstance(subcon, cs.Construct): # type: ignore + raise ValueError( + f"`subcon` parameter has to be an `Construct` object but is {type(subcon)}" + ) + + # create construct format + enum_constr = TFlagsConstruct(subcon, cls) + + # save construct format and make the class compatible to `Constructable` protocol + setattr(cls, "__construct__", lambda: enum_constr) + + return cls + + if t.TYPE_CHECKING: + + @classmethod + def __construct__( + cls: "t.Type[FlagsEnumType]", + ) -> "TFlagsConstruct[FlagsEnumType]": + ... -FlagsEnumType = t.TypeVar("FlagsEnumType", bound=FlagsEnumBase) +FlagsEnumType = t.TypeVar("FlagsEnumType", bound=TFlags) -class TFlagsEnum(Adapter[int, int, FlagsEnumType, FlagsEnumType]): +class TFlagsConstruct(Adapter[int, int, FlagsEnumType, FlagsEnumType]): """ - Typed enum. + Typed flags. """ if t.TYPE_CHECKING: def __new__( cls, subcon: Construct[int, int], enum_type: t.Type[FlagsEnumType] - ) -> "TFlagsEnum[FlagsEnumType]": + ) -> "TFlagsConstruct[FlagsEnumType]": ... def __init__(self, subcon: Construct[int, int], enum_type: t.Type[FlagsEnumType]): - if not issubclass(enum_type, FlagsEnumBase): + 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 # 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: return self.enum_type(obj) From 9a9b4ab95a2df28683294993bbe7992c97609a2a Mon Sep 17 00:00:00 2001 From: Tim Rid <6593626+timrid@users.noreply.github.com> Date: Sun, 13 Feb 2022 16:59:01 +0100 Subject: [PATCH 05/12] fixed type error --- construct_typed/dataclass_struct.py | 4 ++-- tests/test_typed.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/construct_typed/dataclass_struct.py b/construct_typed/dataclass_struct.py index 23a00eb..f9f9705 100644 --- a/construct_typed/dataclass_struct.py +++ b/construct_typed/dataclass_struct.py @@ -118,8 +118,8 @@ class DataclassConstruct(Adapter[t.Any, t.Any, T, T]): dc_type: t.Type[T], reverse: bool = False, ) -> None: - if not isinstance(dc_type, DataclassStruct): - raise TypeError(f"'{repr(dc_type)}' has to be a 'DataclassStruct'") + 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 diff --git a/tests/test_typed.py b/tests/test_typed.py index d74e025..b76ff91 100644 --- a/tests/test_typed.py +++ b/tests/test_typed.py @@ -6,7 +6,7 @@ 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 from .declarativeunittest import common, raises, setattrs From de76415ac58bb022243226f64bccb06c75191dc5 Mon Sep 17 00:00:00 2001 From: Tim Rid <6593626+timrid@users.noreply.github.com> Date: Sun, 13 Feb 2022 17:02:46 +0100 Subject: [PATCH 06/12] changed vscode test configuration to use the native testing api instead of the Test-Explorer --- .vscode/launch.json | 6 ------ .vscode/settings.json | 21 +++++++++++---------- 2 files changed, 11 insertions(+), 16 deletions(-) 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 From 7c68aeecd486aaaff5cb189273658eb5f32dd6db Mon Sep 17 00:00:00 2001 From: Tim Rid <6593626+timrid@users.noreply.github.com> Date: Sun, 13 Feb 2022 17:19:13 +0100 Subject: [PATCH 07/12] adapted all tests to the new api --- construct_typed/__init__.py | 2 + tests/test_typed.py | 177 ++++++++++++------------------------ 2 files changed, 61 insertions(+), 118 deletions(-) diff --git a/construct_typed/__init__.py b/construct_typed/__init__.py index 10e5b12..86e7d7a 100644 --- a/construct_typed/__init__.py +++ b/construct_typed/__init__.py @@ -1,3 +1,4 @@ +from construct_typed.generic import construct from construct_typed.dataclass_struct import ( DataclassBitStruct, DataclassStruct, @@ -16,6 +17,7 @@ from construct_typed.tenum import TEnum, TFlags, TEnumConstruct, TFlagsConstruct __all__ = [ "DataclassBitStruct", "DataclassStruct", + "construct", "csfield", "TEnum", "TEnumConstruct", diff --git a/tests/test_typed.py b/tests/test_typed.py index b76ff91..b69e50c 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, DataclassStruct, csfield +from construct_typed import ( + DataclassBitStruct, + DataclassStruct, + csfield, + construct, + 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 = construct(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), + construct(Image), b"\x01\x0212", Image(width=1, height=2, pixels=b"12"), ) # check __getattr__ - c = cst.DataclassStruct(Image) + c = Image.__construct__() 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), + construct(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(construct(InnerDataclass)) common( - DataclassStruct(TestContainer), + construct(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), + construct(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), + construct(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, + construct(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), + construct(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), + construct(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 = construct(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 = construct(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(construct(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.__construct__() 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), + construct(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.__construct__() + 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 = construct(TestEnum) common(d, b"\x01", TestEnum.one, 1) common(d, b"\xff", TestEnum(255), 1) @@ -375,57 +338,35 @@ 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(construct(TestEnum)) b: int = csfield(cs.Int8ub) common( - DataclassStruct(TestContainer), + construct(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(construct(TestEnum).build, TestContainer(a=1, b=2)) == TypeError # type: ignore ) def test_tenum_flags() -> None: - class TestEnum(cst.FlagsEnumBase): + class TestEnum(TFlags, subcon=cs.Byte): one = 1 two = 2 four = 4 eight = 8 - d = cst.TFlagsEnum(cs.Byte, TestEnum) + d = construct(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" From 8525c04165e617b4b0cd326800d858505597728b Mon Sep 17 00:00:00 2001 From: Tim Rid <6593626+timrid@users.noreply.github.com> Date: Sun, 13 Feb 2022 19:29:56 +0100 Subject: [PATCH 08/12] added custom _EnumMeta, because __init_subclass__ is not working correctly together with the standard enum.EnumMeta... --- construct_typed/tenum.py | 128 +++++++++++++++++++++++---------------- 1 file changed, 77 insertions(+), 51 deletions(-) diff --git a/construct_typed/tenum.py b/construct_typed/tenum.py index 4a84584..47b089c 100644 --- a/construct_typed/tenum.py +++ b/construct_typed/tenum.py @@ -8,35 +8,78 @@ from .generic import * T = t.TypeVar("T") -# ## TEnum ############################################################################################################ -class TEnum(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 = 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, "__construct__", lambda: enum_constr) # type: ignore + + return cls + + +# ## TEnumConstruct ############################################################################################################ +class TEnum(enum.IntEnum, metaclass=_EnumMeta): """ Base class for an Enum used in `construct_typed.TEnumConstruct`. This class extends the standard `enum.IntEnum`, so that missing values are automatically generated. """ - @classmethod - def __init_subclass__( - cls, - subcon: "cs.Construct[t.Any, t.Any]", - **kwargs: t.Any, - ): - super().__init_subclass__(**kwargs) + 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]", + ): + ... - # validate types - if not isinstance(subcon, cs.Construct): # type: ignore - raise ValueError( - f"`subcon` parameter has to be an `Construct` object but is {type(subcon)}" - ) - - # create construct format - enum_constr = TEnumConstruct(subcon, cls) - - # save construct format and make the class compatible to `Constructable` protocol - setattr(cls, "__construct__", lambda: enum_constr) - - return cls + @classmethod + def __construct__(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. @@ -59,12 +102,6 @@ class TEnum(enum.IntEnum): pseudo_member = cls._value2member_map_.setdefault(value, new_member) # type: ignore return pseudo_member # type: ignore - if t.TYPE_CHECKING: - - @classmethod - def __construct__(cls: "t.Type[EnumType]") -> "TEnumConstruct[EnumType]": - ... - EnumType = t.TypeVar("EnumType", bound=TEnum) @@ -108,31 +145,20 @@ class TEnumConstruct(Adapter[int, int, EnumType, EnumType]): "'{}' has to be of type {}".format(repr(obj), repr(self.enum_type)) ) + # ## TFlags ####################################################################################################### -class TFlags(enum.IntFlag): - @classmethod - def __init_subclass__( - cls, - subcon: "cs.Construct[t.Any, t.Any]", - **kwargs: t.Any, - ): - super().__init_subclass__(**kwargs) - - # validate types - if not isinstance(subcon, cs.Construct): # type: ignore - raise ValueError( - f"`subcon` parameter has to be an `Construct` object but is {type(subcon)}" - ) - - # create construct format - enum_constr = TFlagsConstruct(subcon, cls) - - # save construct format and make the class compatible to `Constructable` protocol - setattr(cls, "__construct__", lambda: enum_constr) - - return cls - +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]", + ): + ... @classmethod def __construct__( From e99dd5d752e7413a8f58919b620afa4026703fde Mon Sep 17 00:00:00 2001 From: Tim Rid <6593626+timrid@users.noreply.github.com> Date: Sun, 13 Feb 2022 19:30:35 +0100 Subject: [PATCH 09/12] some renaming --- construct_typed/tenum.py | 20 ++++++++++---------- tests/test_typed.py | 20 ++++++++++---------- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/construct_typed/tenum.py b/construct_typed/tenum.py index 47b089c..b146549 100644 --- a/construct_typed/tenum.py +++ b/construct_typed/tenum.py @@ -162,15 +162,15 @@ class TFlags(enum.IntFlag, metaclass=_EnumMeta): @classmethod def __construct__( - cls: "t.Type[FlagsEnumType]", - ) -> "TFlagsConstruct[FlagsEnumType]": + cls: "t.Type[FlagsType]", + ) -> "TFlagsConstruct[FlagsType]": ... -FlagsEnumType = t.TypeVar("FlagsEnumType", bound=TFlags) +FlagsType = t.TypeVar("FlagsType", bound=TFlags) -class TFlagsConstruct(Adapter[int, int, FlagsEnumType, FlagsEnumType]): +class TFlagsConstruct(Adapter[int, int, FlagsType, FlagsType]): """ Typed flags. """ @@ -178,28 +178,28 @@ class TFlagsConstruct(Adapter[int, int, FlagsEnumType, FlagsEnumType]): if t.TYPE_CHECKING: def __new__( - cls, subcon: Construct[int, int], enum_type: t.Type[FlagsEnumType] - ) -> "TFlagsConstruct[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]): + 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(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(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/tests/test_typed.py b/tests/test_typed.py index b69e50c..8efdbb5 100644 --- a/tests/test_typed.py +++ b/tests/test_typed.py @@ -359,19 +359,19 @@ def test_tenum_in_dataclass_struct() -> None: ) -def test_tenum_flags() -> None: - class TestEnum(TFlags, subcon=cs.Byte): +def test_tflags() -> None: + class TestFlags(TFlags, subcon=cs.Byte): one = 1 two = 2 four = 4 eight = 8 - d = construct(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 = construct(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 From 94a8097bec7212c820ef76e3e23257342cd12ae6 Mon Sep 17 00:00:00 2001 From: Tim Rid <6593626+timrid@users.noreply.github.com> Date: Sun, 13 Feb 2022 19:48:00 +0100 Subject: [PATCH 10/12] added this_struct to public interface --- construct_typed/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/construct_typed/__init__.py b/construct_typed/__init__.py index 86e7d7a..1c11365 100644 --- a/construct_typed/__init__.py +++ b/construct_typed/__init__.py @@ -3,6 +3,7 @@ from construct_typed.dataclass_struct import ( DataclassBitStruct, DataclassStruct, csfield, + this_struct ) from construct_typed.generic import ( Adapter, @@ -23,6 +24,7 @@ __all__ = [ "TEnumConstruct", "TFlags", "TFlagsConstruct", + "this_struct", "Adapter", "ConstantOrContextLambda", "Construct", From be5ae240fb65c3662331fc1dee3ed0c5aac88469 Mon Sep 17 00:00:00 2001 From: Tim Rid <6593626+timrid@users.noreply.github.com> Date: Sat, 19 Feb 2022 12:46:28 +0100 Subject: [PATCH 11/12] renamed `construct` to `constr` so that there are no naming collisions with the construct package name --- construct_typed/__init__.py | 4 +-- construct_typed/dataclass_struct.py | 6 ++-- construct_typed/generic.py | 6 ++-- construct_typed/tenum.py | 6 ++-- tests/test_typed.py | 46 ++++++++++++++--------------- 5 files changed, 34 insertions(+), 34 deletions(-) diff --git a/construct_typed/__init__.py b/construct_typed/__init__.py index 1c11365..56c0e57 100644 --- a/construct_typed/__init__.py +++ b/construct_typed/__init__.py @@ -1,4 +1,4 @@ -from construct_typed.generic import construct +from construct_typed.generic import constr from construct_typed.dataclass_struct import ( DataclassBitStruct, DataclassStruct, @@ -18,7 +18,7 @@ from construct_typed.tenum import TEnum, TFlags, TEnumConstruct, TFlagsConstruct __all__ = [ "DataclassBitStruct", "DataclassStruct", - "construct", + "constr", "csfield", "TEnum", "TEnumConstruct", diff --git a/construct_typed/dataclass_struct.py b/construct_typed/dataclass_struct.py index f9f9705..d6a8566 100644 --- a/construct_typed/dataclass_struct.py +++ b/construct_typed/dataclass_struct.py @@ -78,7 +78,7 @@ def csfield( init = True default = dataclasses.MISSING - # Set default values in case of special sucons + # 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 @@ -249,7 +249,7 @@ class DataclassStruct: _replace_this_struct(constr, dc_constr) # save construct format and make the class compatible to `Constructable` protocol - setattr(cls, "__construct__", lambda: constr) + setattr(cls, "__constr__", lambda: constr) return cls @@ -303,7 +303,7 @@ class DataclassStruct: if t.TYPE_CHECKING: @classmethod - def __construct__(cls: t.Type[T]) -> "DataclassConstruct[T]": + def __constr__(cls: t.Type[T]) -> "DataclassConstruct[T]": ... diff --git a/construct_typed/generic.py b/construct_typed/generic.py index 75c0ac2..b988444 100644 --- a/construct_typed/generic.py +++ b/construct_typed/generic.py @@ -43,16 +43,16 @@ else: @t.runtime_checkable class Constructable(t.Protocol[ParsedType, BuildTypes]): - def __construct__(self) -> "Construct[ParsedType, BuildTypes]": + def __constr__(self) -> "Construct[ParsedType, BuildTypes]": raise NotImplementedError -def construct( +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.__construct__() + constr = constr.__constr__() return constr diff --git a/construct_typed/tenum.py b/construct_typed/tenum.py index b146549..2cf5316 100644 --- a/construct_typed/tenum.py +++ b/construct_typed/tenum.py @@ -52,7 +52,7 @@ class _EnumMeta(enum.EnumMeta): enum_constr = None # save construct format and make the class compatible to `Constructable` protocol - setattr(cls, "__construct__", lambda: enum_constr) # type: ignore + setattr(cls, "__constr__", lambda: enum_constr) # type: ignore return cls @@ -78,7 +78,7 @@ class TEnum(enum.IntEnum, metaclass=_EnumMeta): ... @classmethod - def __construct__(cls: "t.Type[EnumType]") -> "TEnumConstruct[EnumType]": + def __constr__(cls: "t.Type[EnumType]") -> "TEnumConstruct[EnumType]": ... # Extend the enum type with __missing__ method. So if a enum value @@ -161,7 +161,7 @@ class TFlags(enum.IntFlag, metaclass=_EnumMeta): ... @classmethod - def __construct__( + def __constr__( cls: "t.Type[FlagsType]", ) -> "TFlagsConstruct[FlagsType]": ... diff --git a/tests/test_typed.py b/tests/test_typed.py index 8efdbb5..ef0226b 100644 --- a/tests/test_typed.py +++ b/tests/test_typed.py @@ -7,7 +7,7 @@ from construct_typed import ( DataclassBitStruct, DataclassStruct, csfield, - construct, + constr, TEnum, TFlags, ) @@ -61,7 +61,7 @@ def test_dataclass_str_repr() -> None: width: int = csfield(cs.Int8ub) height: int = csfield(cs.Int8ub) - format = construct(Image) + format = constr(Image) obj = Image(width=3, height=2) assert ( str(obj) @@ -81,13 +81,13 @@ def test_dataclass_struct() -> None: pixels: bytes = csfield(cs.Bytes(cs.this.height * cs.this.width)) common( - construct(Image), + constr(Image), b"\x01\x0212", Image(width=1, height=2, pixels=b"12"), ) # check __getattr__ - c = Image.__construct__() + c = Image.__constr__() assert c.width.name == "width" assert c.height.name == "height" assert c.width.subcon is cs.Int8ub @@ -100,7 +100,7 @@ def test_dataclass_struct_reverse() -> None: b: int = csfield(cs.Int8ub) common( - construct(TestContainer), + constr(TestContainer), b"\x02\x00\x01", TestContainer(a=1, b=2), 3, @@ -114,10 +114,10 @@ def test_dataclass_struct_nested() -> None: c: bytes = csfield(cs.Bytes(cs.this._.length)) length: int = csfield(cs.Byte) - a: InnerDataclass = csfield(construct(InnerDataclass)) + a: InnerDataclass = csfield(constr(InnerDataclass)) common( - construct(TestContainer), + constr(TestContainer), b"\x02\x01\xF1\xF2", TestContainer(length=2, a=TestContainer.InnerDataclass(b=1, c=b"\xF1\xF2")), ) @@ -135,7 +135,7 @@ def test_dataclass_struct_default_field() -> None: ) common( - construct(Image), + constr(Image), b"\x02\x03\x00\x00\x00\x00\x00\x00", setattrs(Image(2, 3), pixels=bytes(6)), sample_building=Image(2, 3), @@ -147,7 +147,7 @@ def test_dataclass_struct_const_field() -> None: const_field: t.Optional[bytes] = csfield(cs.Const(b"\x00")) common( - construct(TestContainer), + constr(TestContainer), bytes(1), setattrs(TestContainer(), const_field=b"\x00"), 1, @@ -155,7 +155,7 @@ def test_dataclass_struct_const_field() -> None: assert ( raises( - construct(TestContainer).build, + constr(TestContainer).build, setattrs(TestContainer(), const_field=b"\x01"), ) == cs.ConstError @@ -167,7 +167,7 @@ def test_dataclass_struct_array_field() -> None: array_field: t.List[int] = csfield(cs.Array(5, cs.Int8ub)) common( - construct(TestContainer), + constr(TestContainer), bytes(5), TestContainer(array_field=[0, 0, 0, 0, 0]), 5, @@ -182,7 +182,7 @@ def test_dataclass_struct_anonymus_fields_1() -> None: _4: None = csfield(cs.Terminated) common( - construct(TestContainer), + constr(TestContainer), bytes(2), setattrs(TestContainer(), _1=b"\x00"), cs.SizeofError, @@ -196,7 +196,7 @@ def test_dataclass_struct_anonymus_fields_2() -> None: _3: None = csfield(cs.Pass) _4: None = csfield(cs.Terminated) - d = construct(TestContainer) + d = constr(TestContainer) assert d.build(TestContainer()) == d.build(TestContainer()) @@ -219,7 +219,7 @@ def test_dataclass_struct_overloaded_method() -> None: update: int = csfield(cs.Int8ul) values: int = csfield(cs.Int8ul) - d = construct(TestContainer) + d = constr(TestContainer) obj = d.parse( d.build( TestContainer( @@ -266,7 +266,7 @@ def test_dataclass_struct_wrong_container() -> None: b: int = csfield(cs.Int8ub) assert ( - raises(construct(TestContainer1).build, TestContainer2(a=1, b=2)) == TypeError + raises(constr(TestContainer1).build, TestContainer2(a=1, b=2)) == TypeError ) @@ -284,7 +284,7 @@ def test_dataclass_struct_doc() -> None: """, ) - format = TestContainer.__construct__() + 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" @@ -302,14 +302,14 @@ def test_dataclass_bitstruct() -> None: c: int = csfield(cs.BitsInteger(8)) common( - construct(TestContainer), + constr(TestContainer), b"\xFD\x12", TestContainer(a=0x7E, b=1, c=0x12), 2, ) # check __getattr__ - c = TestContainer.__construct__() + c = TestContainer.__constr__() assert c.subcon.a.name == "a" assert c.subcon.b.name == "b" assert c.subcon.c.name == "c" @@ -325,7 +325,7 @@ def test_tenum() -> None: four = 4 eight = 8 - d = construct(TestEnum) + d = constr(TestEnum) common(d, b"\x01", TestEnum.one, 1) common(d, b"\xff", TestEnum(255), 1) @@ -344,18 +344,18 @@ def test_tenum_in_dataclass_struct() -> None: b = 2 class TestContainer(DataclassStruct): - a: TestEnum = csfield(construct(TestEnum)) + a: TestEnum = csfield(constr(TestEnum)) b: int = csfield(cs.Int8ub) common( - construct(TestContainer), + constr(TestContainer), b"\x01\x02", TestContainer(a=TestEnum.a, b=2), 2, ) assert ( - raises(construct(TestEnum).build, TestContainer(a=1, b=2)) == TypeError # type: ignore + raises(constr(TestEnum).build, TestContainer(a=1, b=2)) == TypeError # type: ignore ) @@ -366,7 +366,7 @@ def test_tflags() -> None: four = 4 eight = 8 - d = construct(TestFlags) + 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" From 75dbd8d8228aa6111fa4c5b22f01230c8359f62c Mon Sep 17 00:00:00 2001 From: Tim Rid <6593626+timrid@users.noreply.github.com> Date: Sat, 19 Feb 2022 14:10:18 +0100 Subject: [PATCH 12/12] fixed some mypy issues --- construct_typed/dataclass_struct.py | 53 ++++++++++++++--------------- construct_typed/tenum.py | 22 ++++++------ tests/declarativeunittest.pyi | 4 +-- 3 files changed, 39 insertions(+), 40 deletions(-) diff --git a/construct_typed/dataclass_struct.py b/construct_typed/dataclass_struct.py index d6a8566..ea89f5a 100644 --- a/construct_typed/dataclass_struct.py +++ b/construct_typed/dataclass_struct.py @@ -31,29 +31,29 @@ def __dataclass_transform__( DATACLASS_METADATA_KEY = "__construct_typed_subcon" -if t.TYPE_CHECKING: - # 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: - ... +# 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: - ... + +@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], + subcon: "Construct[ParsedType, t.Any]", doc: t.Optional[str] = None, parsed: t.Optional[t.Callable[[t.Any, Context], None]] = None, init: bool = True, @@ -119,7 +119,9 @@ class DataclassConstruct(Adapter[t.Any, t.Any, T, T]): reverse: bool = False, ) -> None: if not issubclass(dc_type, DataclassStruct): - raise TypeError(f"'{repr(dc_type)}' has to be a subclass of '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 @@ -155,7 +157,7 @@ class DataclassConstruct(Adapter[t.Any, t.Any, T, T]): dc_init[field.name] = value # create object of dataclass - dc = self.dc_type(**dc_init) # type: ignore + 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: @@ -185,7 +187,7 @@ class DataclassConstruct(Adapter[t.Any, t.Any, T, T]): this_struct: Construct[t.Any, t.Any] = Construct() -def _replace_this_struct(constr: "Construct[t.Any, t.Any]", replacement: t.Any): +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: @@ -231,7 +233,7 @@ class DataclassStruct: 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") @@ -251,8 +253,6 @@ class DataclassStruct: # save construct format and make the class compatible to `Constructable` protocol setattr(cls, "__constr__", lambda: constr) - return cls - # 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: @@ -336,6 +336,5 @@ class DataclassBitStruct(DataclassStruct): 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 - return cls diff --git a/construct_typed/tenum.py b/construct_typed/tenum.py index 2cf5316..83f1d8c 100644 --- a/construct_typed/tenum.py +++ b/construct_typed/tenum.py @@ -12,22 +12,22 @@ class _EnumMeta(enum.EnumMeta): @classmethod def __prepare__( metacls, # type: ignore - name: str, - bases: t.Tuple[type, ...], + __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) + 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], + __name: str, + __bases: t.Tuple[type, ...], + __namespace: t.Dict[str, t.Any], **kwargs: t.Any, ) -> T: # create new enum object - cls = super().__new__(metacls, name, bases, namespace) # type: ignore + cls: T = super().__new__(metacls, __name, __bases, __namespace) # type: ignore # if the `TEnum` class is created, there are no parameters if len(kwargs) == 0: @@ -44,9 +44,9 @@ class _EnumMeta(enum.EnumMeta): raise ValueError(f"unsupported parameter(s) detected: {unsupp_parm}") # create construct format - if TEnum in bases: + if TEnum in __bases: enum_constr = TEnumConstruct(subcon, cls) # type: ignore - elif TFlags in bases: + elif TFlags in __bases: enum_constr = TFlagsConstruct(subcon, cls) # type: ignore else: enum_constr = None @@ -74,7 +74,7 @@ class TEnum(enum.IntEnum, metaclass=_EnumMeta): def __init_subclass__( cls, subcon: "cs.Construct[t.Any, t.Any]", - ): + ) -> None: ... @classmethod @@ -157,7 +157,7 @@ class TFlags(enum.IntFlag, metaclass=_EnumMeta): def __init_subclass__( cls, subcon: "cs.Construct[t.Any, t.Any]", - ): + ) -> None: ... @classmethod 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]] = ...,