diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index e09f947..4a3e3db 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -1,10 +1,6 @@ name: CI -on: - push: - pull_request: - workflow_dispatch: - workflow_call: +on: [push, pull_request] jobs: build: @@ -12,7 +8,7 @@ jobs: strategy: matrix: os: ['ubuntu-latest', 'windows-latest'] - python-version: [ '3.9', '3.10', '3.11', '3.12', '3.13' ] + python-version: [ '3.7', '3.8', '3.9' ] runs-on: ${{ matrix.os }} name: OS ${{ matrix.os }}, Python ${{ matrix.python-version }} @@ -20,26 +16,25 @@ jobs: steps: # Checks out a copy of your repository on the machine - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v1 # Setup python - name: Setup python - uses: actions/setup-python@v4 + uses: actions/setup-python@v1 with: python-version: ${{ matrix.python-version }} architecture: x64 # Setup node.js (for pyright) - name: Setup node.js (for pyright) - uses: actions/setup-node@v3 + uses: actions/setup-node@v2 with: - node-version: 16 + node-version: '14' # Install pyright - name: Install pyright run: | npm install -g pyright - pyright --version # Install this package - name: Install this package @@ -66,30 +61,3 @@ jobs: - name: Run pyright run: | pyright - - create_wheel_and_sdist: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v3 - - - name: Set up Python - uses: actions/setup-python@v4 - with: - python-version: '3.13' - architecture: x64 - - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install wheel build - - - name: Build wheel and sdist - run: | - python -m build - - - name: Upload wheel and sdist as artifact - uses: actions/upload-artifact@v4 - with: - name: Package-Distributions-construct-typing - path: dist/ \ No newline at end of file diff --git a/.github/workflows/python-publish.yml b/.github/workflows/python-publish.yml index ea14263..4e1ef42 100644 --- a/.github/workflows/python-publish.yml +++ b/.github/workflows/python-publish.yml @@ -1,3 +1,6 @@ +# This workflows will upload a Python Package using Twine when a release is created +# For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries + name: Upload Python Package on: @@ -5,26 +8,24 @@ on: types: [created] jobs: - create_wheel_and_sdist: - name: create_wheel_and_sdist - uses: ./.github/workflows/main.yml - deploy: - needs: [ create_wheel_and_sdist ] + runs-on: ubuntu-latest - - environment: pypi - permissions: - id-token: write # IMPORTANT: this permission is mandatory for Trusted Publishing steps: - - uses: actions/checkout@v3 - - - name: Download artifacts - uses: actions/download-artifact@v4 + - uses: actions/checkout@v2 + - name: Set up Python + uses: actions/setup-python@v2 with: - name: Package-Distributions-construct-typing - path: ./dist - - - name: Publish package distributions to PyPI - uses: pypa/gh-action-pypi-publish@release/v1 + python-version: '3.x' + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install setuptools wheel twine + - name: Build and publish + env: + TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} + TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} + run: | + python setup.py sdist bdist_wheel + twine upload dist/* diff --git a/.gitignore b/.gitignore index 1d9e0fe..b3d4398 100644 --- a/.gitignore +++ b/.gitignore @@ -129,6 +129,3 @@ dmypy.json example_737 example_888 example_ksy.ksy - -# Test stuff -devtest/ \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json index d94289d..d8f4661 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -9,7 +9,8 @@ "type": "python", "request": "launch", "program": "${file}", - "console": "integratedTerminal" + "console": "integratedTerminal", + "justMyCode": false }, { "name": "Debug Tests", diff --git a/.vscode/settings.json b/.vscode/settings.json index 75b5040..2da24ea 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,23 +1,26 @@ { - // static analysis + "python.pythonPath": "python", "python.languageServer": "Pylance", + // "python.testing.unittestEnabled": false, + // "python.testing.nosetestsEnabled": false, + // "python.testing.pytestEnabled": true, + "pythonTestExplorer.testFramework": "pytest", + "python.formatting.provider": "black", + "python.sortImports.path": "isort", + "python.sortImports.args": [ + "--profile=black", + ], + // "[python]": { + // "editor.codeActionsOnSave": { + // "source.organizeImports": true + // } + // } "python.analysis.typeCheckingMode": "strict", "python.analysis.autoImportCompletions": false, "python.analysis.diagnosticSeverityOverrides": { "reportPrivateUsage": "information", "reportUntypedNamedTuple": "information", }, - - // formating - "python.formatting.provider": "black", - - // sorting - "python.sortImports.path": "isort", - "python.sortImports.args": [ - "--profile=black", - ], - - // tests "python.testing.pytestArgs": [ "tests" ], diff --git a/README.md b/README.md index c4bac18..25f53fb 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,3 @@ -## Modified version of "construct-typing" module used in my projects. -This modification features: -- **[EnhancedDataclassMixin](https://github.com/waszil/construct-typing/commit/479b51344bfd95149596a75ee574ac2e63c032df) with additional features** -- **ConstantOrContextLambda2 type** -- **Typing for Subconstruct** -- **Type hint for Computed** -- **Switch typing fixes** - -The original README.md file was described down below: # construct-typing [![PyPI](https://img.shields.io/pypi/v/construct-typing)](https://pypi.org/project/construct-typing/) ![PyPI - Implementation](https://img.shields.io/pypi/implementation/construct-typing) @@ -77,29 +68,28 @@ A short example: import dataclasses import typing as t from construct import Array, Byte, Const, Int8ub, this -from construct_typed import DataclassMixin, DataclassStruct, EnumBase, TEnum, csfield +from construct_typed import AttrsStruct, Enum, construct, attrs_field -class Orientation(EnumBase): +class Orientation(Enum, constr=Int8ub): # TODO: Implement this HORIZONTAL = 0 VERTICAL = 1 -@dataclasses.dataclass -class Image(DataclassMixin): - signature: bytes = csfield(Const(b"BMP")) - orientation: Orientation = csfield(TEnum(Int8ub, Orientation)) - width: int = csfield(Int8ub) - height: int = csfield(Int8ub) - pixels: t.List[int] = csfield(Array(this.width * this.height, Byte)) +class Image(AttrsStruct): + signature: bytes = attrs_field(Const(b"BMP")) + orientation: Orientation = attrs_field(construct(Orientation)) # TODO: Implement this + width: int = attrs_field(Int8ub) + height: int = attrs_field(Int8ub) + pixels: t.List[int] = attrs_field(Array(this.width * this.height, Byte)) -format = DataclassStruct(Image) +fmt = construct(Image) obj = Image( orientation=Orientation.VERTICAL, width=3, height=2, pixels=[7, 8, 9, 11, 12, 13], ) -print(format.build(obj)) -print(format.parse(b"BMP\x01\x03\x02\x07\x08\t\x0b\x0c\r")) +print(fmt.build(obj)) +print(fmt.parse(b"BMP\x01\x03\x02\x07\x08\t\x0b\x0c\r")) ``` Output: ``` diff --git a/construct-stubs/__init__.pyi b/construct-stubs/__init__.pyi index 2cad48a..858384d 100644 --- a/construct-stubs/__init__.pyi +++ b/construct-stubs/__init__.pyi @@ -3,10 +3,6 @@ from construct.debug import * from construct.expr import * from construct.lib import * from construct.version import * -from construct import lib - -__author__: str -__version__: str #=============================================================================== # exposed names diff --git a/construct-stubs/core.pyi b/construct-stubs/core.pyi index 7ed1af6..010f471 100644 --- a/construct-stubs/core.pyi +++ b/construct-stubs/core.pyi @@ -17,10 +17,6 @@ from construct.lib import ( ListType, RebufferedBytesIO, ) -from cryptography.hazmat.primitives.ciphers import Cipher -from cryptography.hazmat.primitives.ciphers.aead import AESCCM, AESGCM, ChaCha20Poly1305 -from cryptography.hazmat.primitives.ciphers.modes import Mode -from typing_extensions import Buffer, TypeAlias # unfortunately, there are a few duplications with "typing", e.g. Union and Optional, which is why the t. prefix must be used everywhere @@ -29,8 +25,7 @@ from typing_extensions import Buffer, TypeAlias # - Higher Kinded Types: https://github.com/python/typing/issues/548 # - Higher Kinded Types: https://sobolevn.me/2020/10/higher-kinded-types-in-python -ReadableBuffer: TypeAlias = Buffer -StreamType = t.IO[bytes] +StreamType = t.BinaryIO FilenameType = t.Union[str, bytes, os.PathLike[str], os.PathLike[bytes]] PathType = str ContextKWType = t.Any @@ -70,37 +65,25 @@ class RawCopyError(ConstructError): ... class RotationError(ConstructError): ... class ChecksumError(ConstructError): ... class CancelParsing(ConstructError): ... -class CipherError(ConstructError): ... # =============================================================================== # used internally # =============================================================================== def stream_read( - stream: StreamType, length: int, path: t.Optional[PathType] + stream: t.BinaryIO, length: int, path: t.Optional[PathType] ) -> bytes: ... -def stream_read_entire(stream: StreamType, path: t.Optional[PathType]) -> bytes: ... +def stream_read_entire(stream: t.BinaryIO, path: t.Optional[PathType]) -> bytes: ... def stream_write( - stream: StreamType, data: bytes, length: int, path: t.Optional[PathType] + stream: t.BinaryIO, data: bytes, length: int, path: t.Optional[PathType] ) -> None: ... def stream_seek( - stream: StreamType, offset: int, whence: int, path: t.Optional[PathType] + stream: t.BinaryIO, offset: int, whence: int, path: t.Optional[PathType] ) -> int: ... -def stream_tell(stream: StreamType, path: t.Optional[PathType]) -> int: ... -def stream_size(stream: StreamType) -> int: ... -def stream_iseof(stream: StreamType) -> bool: ... +def stream_tell(stream: t.BinaryIO, path: t.Optional[PathType]) -> int: ... +def stream_size(stream: t.BinaryIO) -> int: ... +def stream_iseof(stream: t.BinaryIO) -> bool: ... def evaluate(param: ConstantOrContextLambda2[T], context: Context) -> T: ... -class BytesIOWithOffsets(io.BytesIO): - @staticmethod - def from_reading( - stream: StreamType, length: int, path: PathType - ) -> BytesIOWithOffsets: ... - def __init__( - self, contents: bytes, parent_stream: StreamType, offset: int - ) -> None: ... - def tell(self) -> int: ... - def seek(self, offset: int, whence: int = ...) -> int: ... - # =============================================================================== # abstract constructs # =============================================================================== @@ -112,7 +95,7 @@ class Construct(t.Generic[ParsedType, BuildTypes]): docs: str flagbuildnone: bool parsed: t.Optional[t.Callable[[ParsedType, Context], None]] - def parse(self, data: ReadableBuffer, **contextkw: ContextKWType) -> ParsedType: ... + def parse(self, data: bytes, **contextkw: ContextKWType) -> ParsedType: ... def parse_stream( self, stream: StreamType, **contextkw: ContextKWType ) -> ParsedType: ... @@ -122,17 +105,15 @@ class Construct(t.Generic[ParsedType, BuildTypes]): def build(self, obj: BuildTypes, **contextkw: ContextKWType) -> bytes: ... def build_stream( self, obj: BuildTypes, stream: StreamType, **contextkw: ContextKWType - ) -> None: ... + ) -> bytes: ... def build_file( self, obj: BuildTypes, filename: FilenameType, **contextkw: ContextKWType - ) -> None: ... + ) -> bytes: ... def sizeof(self, **contextkw: ContextKWType) -> int: ... def compile( self, filename: FilenameType = ... ) -> Construct[ParsedType, BuildTypes]: ... - def benchmark( - self, sampledata: ReadableBuffer, filename: FilenameType = ... - ) -> str: ... + def benchmark(self, sampledata: bytes, filename: FilenameType = ...) -> str: ... def export_ksy( self, schemaname: str = ..., filename: FilenameType = ... ) -> str: ... @@ -148,22 +129,20 @@ class Construct(t.Generic[ParsedType, BuildTypes]): self, other: t.Union[str, bytes, t.Callable[[ParsedType, Context], None]], ) -> Renamed[ParsedType, BuildTypes]: ... - def __add__(self, other: Construct[t.Any, t.Any]) -> Struct: ... - def __rshift__(self, other: Construct[t.Any, t.Any]) -> Sequence: ... - def __getitem__(self, count: t.Union[int, t.Callable[[Context], int]]) -> Array[ + def __add__( + self, other: Construct[t.Any, t.Any] + ) -> Struct[Container[t.Any], t.Optional[t.Dict[str, t.Any]]]: ... + def __rshift__( + self, other: Construct[t.Any, t.Any] + ) -> Sequence[ListContainer[t.Any], t.Optional[t.List[t.Any]]]: ... + def __getitem__( + self, count: t.Union[int, t.Callable[[Context], int]] + ) -> Array[ ParsedType, BuildTypes, + ListContainer[ParsedType], + t.List[BuildTypes], ]: ... - def _parse( - self, stream: StreamType, context: Context, path: PathType - ) -> ParsedType: ... - def _parsereport( - self, stream: StreamType, context: Context, path: PathType - ) -> ParsedType: ... - def _build( - self, obj: BuildTypes, stream: StreamType, context: Context, path: PathType - ) -> int: ... - def _sizeof(self, context: Context, path: PathType) -> int: ... @t.type_check_only class Context(Container[t.Any]): @@ -190,23 +169,22 @@ class Subconstruct( ): subcon: Construct[SubconParsedType, SubconBuildTypes] @t.overload - def __init__( - self, - subcon: Construct[SubconParsedType, SubconBuildTypes], - ) -> None: ... + def __new__( + cls, subcon: Construct[SubconParsedType, SubconBuildTypes] + ) -> Subconstruct[ + SubconParsedType, SubconBuildTypes, SubconParsedType, SubconBuildTypes + ]: ... @t.overload - def __init__( # type: ignore - self, - *args: t.Any, - **kwargs: t.Any, - ) -> None: ... + def __new__( + cls, *args: t.Any, **kwargs: t.Any + ) -> Subconstruct[SubconParsedType, SubconBuildTypes, ParsedType, BuildTypes]: ... class Adapter( Subconstruct[SubconParsedType, SubconBuildTypes, ParsedType, BuildTypes], ): - def __init__( - self, subcon: Construct[SubconParsedType, SubconBuildTypes] - ) -> None: ... + def __new__( + cls, subcon: Construct[SubconParsedType, SubconBuildTypes] + ) -> Adapter[SubconParsedType, SubconBuildTypes, ParsedType, BuildTypes]: ... def _decode( self, obj: SubconBuildTypes, context: Context, path: PathType ) -> ParsedType: ... @@ -233,34 +211,28 @@ class Tunnel( def _decode(self, data: bytes, context: Context, path: PathType) -> bytes: ... def _encode(self, data: bytes, context: Context, path: PathType) -> bytes: ... -class Compiled(Construct[t.Any, t.Any]): - source: t.Optional[str] - defersubcon: t.Optional[Construct[t.Any, t.Any]] - parsefunc: t.Callable[[StreamType, Context], t.Any] - buildfunc: t.Callable[[t.Any, StreamType, Context], t.Any] - def __init__( - self, - parsefunc: t.Callable[[StreamType, Context], t.Any], - buildfunc: t.Callable[[t.Any, StreamType, Context], t.Any], - ) -> None: ... +# TODO: Compiled # =============================================================================== # bytes and bits # =============================================================================== -class Bytes(Construct[bytes, t.Union[bytes, bytearray, int]]): +class Bytes(Construct[ParsedType, BuildTypes]): length: ConstantOrContextLambda[int] - def __init__( - self, - length: ConstantOrContextLambda[int], - ) -> None: ... + def __new__( + cls, length: ConstantOrContextLambda[int] + ) -> Bytes[bytes, t.Union[bytes, int]]: ... -GreedyBytes: Construct[bytes, t.Union[bytes, bytearray]] +GreedyBytes: Construct[bytes, bytes] -def Bitwise(subcon: Construct[SubconParsedType, SubconBuildTypes]) -> t.Union[ +def Bitwise( + subcon: Construct[SubconParsedType, SubconBuildTypes] +) -> t.Union[ Transformed[SubconParsedType, SubconBuildTypes], Restreamed[SubconParsedType, SubconBuildTypes], ]: ... -def Bytewise(subcon: Construct[SubconParsedType, SubconBuildTypes]) -> t.Union[ +def Bytewise( + subcon: Construct[SubconParsedType, SubconBuildTypes] +) -> t.Union[ Transformed[SubconParsedType, SubconBuildTypes], Restreamed[SubconParsedType, SubconBuildTypes], ]: ... @@ -278,61 +250,46 @@ class FormatField(Construct[ParsedType, BuildTypes]): FORMAT_BOOL = t.Literal["?"] @t.overload def __new__( - cls: "type[FormatField[int, int]]", - endianity: str, - format: FORMAT_INT, + cls, endianity: str, format: FORMAT_INT ) -> FormatField[int, int]: ... @t.overload def __new__( - cls: "type[FormatField[float, float]]", - endianity: str, - format: FORMAT_FLOAT, + cls, endianity: str, format: FORMAT_FLOAT ) -> FormatField[float, float]: ... @t.overload def __new__( - cls: "type[FormatField[bool, bool]]", - endianity: str, - format: FORMAT_BOOL, + cls, endianity: str, format: FORMAT_BOOL ) -> FormatField[bool, bool]: ... @t.overload - def __new__( - cls: "type[FormatField[t.Any, t.Any]]", - endianity: str, - format: str, - ) -> FormatField[t.Any, t.Any]: ... - + def __new__(cls, endianity: str, format: str) -> FormatField[t.Any, t.Any]: ... else: - def __new__( - cls: "type[FormatField[t.Any, t.Any]]", - endianity: str, - format: str, - ) -> FormatField[t.Any, t.Any]: ... + def __new__(cls, endianity: str, format: str) -> FormatField[t.Any, t.Any]: ... -class BytesInteger(Construct[int, int]): +class BytesInteger(Construct[ParsedType, BuildTypes]): length: ConstantOrContextLambda[int] signed: bool swapped: ConstantOrContextLambda[bool] - def __init__( - self, + def __new__( + cls, length: ConstantOrContextLambda[int], signed: bool = ..., swapped: ConstantOrContextLambda[bool] = ..., - ) -> None: ... + ) -> BytesInteger[int, int]: ... -class BitsInteger(Construct[int, int]): +class BitsInteger(Construct[ParsedType, BuildTypes]): length: ConstantOrContextLambda[int] signed: bool swapped: ConstantOrContextLambda[bool] - def __init__( - self, + def __new__( + cls, length: ConstantOrContextLambda[int], signed: bool = ..., swapped: ConstantOrContextLambda[bool] = ..., - ) -> None: ... + ) -> BitsInteger[int, int]: ... -Bit: BitsInteger -Nibble: BitsInteger -Octet: BitsInteger +Bit: BitsInteger[int, int] +Nibble: BitsInteger[int, int] +Octet: BitsInteger[int, int] Int8ub: FormatField[int, int] Int16ub: FormatField[int, int] @@ -378,12 +335,12 @@ Half: FormatField[float, float] Single: FormatField[float, float] Double: FormatField[float, float] -Int24ub: BytesInteger -Int24ul: BytesInteger -Int24un: BytesInteger -Int24sb: BytesInteger -Int24sl: BytesInteger -Int24sn: BytesInteger +Int24ub: BytesInteger[int, int] +Int24ul: BytesInteger[int, int] +Int24un: BytesInteger[int, int] +Int24sb: BytesInteger[int, int] +Int24sl: BytesInteger[int, int] +Int24sn: BytesInteger[int, int] VarInt: Construct[int, int] ZigZag: Construct[int, int] @@ -391,9 +348,7 @@ ZigZag: Construct[int, int] # =============================================================================== # strings # =============================================================================== -possiblestringencodings: t.Dict[str, int] - -class StringEncoded(Construct[str, str]): +class StringEncoded(Construct[ParsedType, BuildTypes]): if sys.version_info >= (3, 8): ENCODING_1 = t.Literal["ascii", "utf8", "utf_8", "u8"] ENCODING_2 = t.Literal["utf16", "utf_16", "u16", "utf_16_be", "utf_16_le"] @@ -402,20 +357,18 @@ class StringEncoded(Construct[str, str]): else: ENCODING = str encoding: ENCODING - def __init__( - self, - subcon: Construct[bytes, bytes], - encoding: ENCODING, - ) -> None: ... + def __new__( + cls, subcon: Construct[ParsedType, BuildTypes], encoding: ENCODING + ) -> StringEncoded[str, str]: ... def PaddedString( length: ConstantOrContextLambda[int], encoding: StringEncoded.ENCODING -) -> StringEncoded: ... +) -> StringEncoded[str, str]: ... def PascalString( lengthfield: Construct[int, int], encoding: StringEncoded.ENCODING -) -> StringEncoded: ... -def CString(encoding: StringEncoded.ENCODING) -> StringEncoded: ... -def GreedyString(encoding: StringEncoded.ENCODING) -> StringEncoded: ... +) -> StringEncoded[str, str]: ... +def CString(encoding: StringEncoded.ENCODING) -> StringEncoded[str, str]: ... +def GreedyString(encoding: StringEncoded.ENCODING) -> StringEncoded[str, str]: ... # =============================================================================== # mappings @@ -428,68 +381,58 @@ class EnumIntegerString(str): @staticmethod def new(intvalue: int, stringvalue: str) -> EnumIntegerString: ... -class Enum( - Adapter[int, int, t.Union[EnumInteger, EnumIntegerString], t.Union[int, str]] -): +class Enum(Adapter[int, int, ParsedType, BuildTypes]): encmapping: t.Dict[str, int] decmapping: t.Dict[int, EnumIntegerString] ksymapping: t.Dict[int, str] - def __init__( - self, + def __new__( + cls, subcon: Construct[int, int], *merge: t.Union[t.Type[enum.IntEnum], t.Type[enum.IntFlag]], - **mapping: int, - ) -> None: ... + **mapping: int + ) -> Enum[t.Union[EnumInteger, EnumIntegerString], t.Union[int, str]]: ... def __getattr__(self, name: str) -> EnumIntegerString: ... class BitwisableString(str): def __or__(self, other: BitwisableString) -> BitwisableString: ... -class FlagsEnum( - Adapter[int, int, Container[bool], t.Union[int, str, t.Dict[str, bool]]] -): +class FlagsEnum(Adapter[int, int, ParsedType, BuildTypes]): flags: t.Dict[str, int] reverseflags: t.Dict[int, str] - def __init__( - self, + def __new__( + cls, subcon: Construct[int, int], *merge: t.Union[t.Type[enum.IntEnum], t.Type[enum.IntFlag]], - **flags: int, - ) -> None: ... + **flags: int + ) -> FlagsEnum[Container[bool], t.Union[int, str, t.Dict[str, bool]]]: ... def __getattr__(self, name: str) -> BitwisableString: ... class Mapping(Adapter[SubconParsedType, SubconBuildTypes, t.Any, t.Any]): decmapping: t.Dict[int, str] encmapping: t.Dict[str, int] - def __init__( - self, + def __new__( + cls, subcon: Construct[SubconParsedType, SubconBuildTypes], mapping: t.Dict[t.Any, t.Any], - ) -> None: ... + ) -> Mapping[t.Any, t.Any]: ... # =============================================================================== # structures and sequences # =============================================================================== # this can maybe made better when variadic generics are available -class Struct(Construct[Container[t.Any], t.Optional[t.Dict[str, t.Any]]]): +class Struct(Construct[ParsedType, BuildTypes]): subcons: t.List[Construct[t.Any, t.Any]] - _subcons: t.Dict[str, Construct[t.Any, t.Any]] - def __init__( - self, - *subcons: Construct[t.Any, t.Any], - **subconskw: Construct[t.Any, t.Any], - ) -> None: ... + def __new__( + cls, *subcons: Construct[t.Any, t.Any], **subconskw: Construct[t.Any, t.Any] + ) -> Struct[Container[t.Any], t.Optional[t.Dict[str, t.Any]]]: ... def __getattr__(self, name: str) -> t.Any: ... # this can maybe made better when variadic generics are available -class Sequence(Construct[ListContainer[t.Any], t.Optional[t.List[t.Any]]]): +class Sequence(Construct[ParsedType, BuildTypes]): subcons: t.List[Construct[t.Any, t.Any]] - _subcons: t.Dict[str, Construct[t.Any, t.Any]] - def __init__( - self, - *subcons: Construct[t.Any, t.Any], - **subconskw: Construct[t.Any, t.Any], - ) -> None: ... + def __new__( + cls, *subcons: Construct[t.Any, t.Any], **subconskw: Construct[t.Any, t.Any] + ) -> Sequence[ListContainer[t.Any], t.Optional[t.List[t.Any]]]: ... def __getattr__(self, name: str) -> t.Any: ... # =============================================================================== @@ -499,40 +442,48 @@ class Array( Subconstruct[ SubconParsedType, SubconBuildTypes, - ListContainer[SubconParsedType], # type: ignore - t.List[SubconBuildTypes], # type: ignore + ParsedType, + BuildTypes, ] ): count: ConstantOrContextLambda[int] discard: bool - def __init__( - self, + def __new__( + cls, count: ConstantOrContextLambda[int], subcon: Construct[SubconParsedType, SubconBuildTypes], discard: bool = ..., - ) -> None: ... + ) -> Array[ + SubconParsedType, + SubconBuildTypes, + ListContainer[SubconParsedType], + t.List[SubconBuildTypes], + ]: ... class GreedyRange( Subconstruct[ SubconParsedType, SubconBuildTypes, - ListContainer[SubconParsedType], # type: ignore - t.List[SubconBuildTypes], # type: ignore + ParsedType, + BuildTypes, ] ): discard: bool - def __init__( - self, - subcon: Construct[SubconParsedType, SubconBuildTypes], - discard: bool = ..., - ) -> None: ... + def __new__( + cls, subcon: Construct[SubconParsedType, SubconBuildTypes], discard: bool = ... + ) -> GreedyRange[ + SubconParsedType, + SubconBuildTypes, + ListContainer[SubconParsedType], + t.List[SubconBuildTypes], + ]: ... class RepeatUntil( Subconstruct[ SubconParsedType, SubconBuildTypes, - ListContainer[SubconParsedType], # type: ignore - t.List[SubconBuildTypes], # type: ignore + ListContainer[SubconParsedType], + t.List[SubconBuildTypes], ] ): predicate: t.Union[ @@ -569,69 +520,67 @@ class Renamed( # =============================================================================== # miscellaneous # =============================================================================== -class Const(Subconstruct[t.Any, t.Any, ParsedType, BuildTypes]): - value: BuildTypes +class Const(Subconstruct[SubconParsedType, SubconBuildTypes, ParsedType, BuildTypes]): + value: SubconBuildTypes @t.overload def __new__( - cls: "type[Const[bytes, t.Optional[bytes]]]", + cls, value: bytes, - ) -> Const[bytes, t.Optional[bytes]]: ... + ) -> Const[None, None, bytes, t.Optional[bytes]]: ... @t.overload def __new__( - cls: "type[Const[SubconParsedType, t.Optional[SubconBuildTypes]]]", + cls, value: SubconBuildTypes, subcon: Construct[SubconParsedType, SubconBuildTypes], - ) -> Const[SubconParsedType, t.Optional[SubconBuildTypes]]: ... + ) -> Const[None, None, SubconParsedType, t.Optional[SubconBuildTypes]]: ... -class Computed(Construct[ParsedType, None]): +class Computed(Construct[ParsedType, BuildTypes]): func: ConstantOrContextLambda2[ParsedType] - def __init__( - self, - func: ConstantOrContextLambda2[ParsedType], - ) -> None: ... + @t.overload + def __new__( + cls, func: ConstantOrContextLambda2[ParsedType] + ) -> Computed[ParsedType, None]: ... + @t.overload + def __new__( + cls, func: ConstantOrContextLambda2[t.Any] + ) -> Computed[t.Any, None]: ... Index: Construct[int, t.Any] -class Rebuild(Subconstruct[SubconParsedType, SubconBuildTypes, SubconParsedType, None]): +class Rebuild(Subconstruct[SubconParsedType, SubconBuildTypes, ParsedType, BuildTypes]): func: ConstantOrContextLambda[SubconBuildTypes] - def __init__( - self, + def __new__( + cls, subcon: Construct[SubconParsedType, SubconBuildTypes], func: ConstantOrContextLambda[SubconBuildTypes], - ) -> None: ... + ) -> Rebuild[SubconParsedType, SubconBuildTypes, SubconParsedType, None]: ... -class Default( - Subconstruct[ +class Default(Subconstruct[SubconParsedType, SubconBuildTypes, ParsedType, BuildTypes]): + value: ConstantOrContextLambda[SubconBuildTypes] + def __new__( + cls, + subcon: Construct[SubconParsedType, SubconBuildTypes], + value: ConstantOrContextLambda[SubconBuildTypes], + ) -> Default[ SubconParsedType, SubconBuildTypes, SubconParsedType, t.Optional[SubconBuildTypes], - ] -): - value: ConstantOrContextLambda[SubconBuildTypes] - def __init__( - self, - subcon: Construct[SubconParsedType, SubconBuildTypes], - value: ConstantOrContextLambda[SubconBuildTypes], - ) -> None: ... + ]: ... -class Check(Construct[None, None]): +class Check(Construct[ParsedType, BuildTypes]): func: ConstantOrContextLambda[bool] - def __init__( - self, - func: ConstantOrContextLambda[bool], - ) -> None: ... + def __new__(cls, func: ConstantOrContextLambda[bool]) -> Check[None, None]: ... -Error: Construct[t.NoReturn, t.NoReturn] +Error: Construct[None, None] class FocusedSeq(Construct[t.Any, t.Any]): subcons: t.List[Construct[t.Any, t.Any]] - _subcons: t.Dict[str, Construct[t.Any, t.Any]] def __init__( self, parsebuildfrom: ConstantOrContextLambda[str], *subcons: Construct[t.Any, t.Any], - **subconskw: Construct[t.Any, t.Any], + **subconskw: Construct[t.Any, t.Any] ) -> None: ... def __getattr__(self, name: str) -> t.Any: ... @@ -643,19 +592,24 @@ class NamedTuple( Adapter[ SubconParsedType, SubconBuildTypes, - t.Tuple[t.Any, ...], - t.Union[t.Tuple[t.Any, ...], t.List[t.Any], t.Dict[str, t.Any]], + ParsedType, + BuildTypes, ] ): tuplename: str tuplefields: str factory: Construct[SubconParsedType, SubconBuildTypes] - def __init__( - self, + def __new__( + cls, tuplename: str, tuplefields: str, subcon: Construct[SubconParsedType, SubconBuildTypes], - ) -> None: ... + ) -> NamedTuple[ + SubconParsedType, + SubconBuildTypes, + t.Tuple[t.Any, ...], + t.Union[t.Tuple[t.Any, ...], t.List[t.Any], t.Dict[str, t.Any]], + ]: ... if sys.version_info >= (3, 8): MSDOS = t.Literal["msdos"] @@ -686,60 +640,63 @@ def Timestamp( K = t.TypeVar("K") V = t.TypeVar("V") -class Hex(Adapter[t.Any, t.Any, ParsedType, BuildTypes]): +class Hex(Adapter[SubconParsedType, SubconBuildTypes, ParsedType, BuildTypes]): @t.overload def __new__( - cls: "type[Hex[HexDisplayedInteger, BuildTypes]]", - subcon: Construct[int, BuildTypes], - ) -> Hex[HexDisplayedInteger, BuildTypes]: ... + cls, subcon: Construct[int, BuildTypes] + ) -> Hex[int, BuildTypes, HexDisplayedInteger, BuildTypes]: ... @t.overload def __new__( - cls: "type[Hex[HexDisplayedBytes, BuildTypes]]", - subcon: Construct[bytes, BuildTypes], - ) -> Hex[HexDisplayedBytes, BuildTypes]: ... + cls, subcon: Construct[bytes, BuildTypes] + ) -> Hex[bytes, BuildTypes, HexDisplayedBytes, BuildTypes]: ... @t.overload def __new__( - cls: "type[Hex[HexDisplayedDict[str, t.Union[int, bytes, SubconParsedType]], BuildTypes,]]", - subcon: Construct[RawCopyObj[SubconParsedType], BuildTypes], + cls, subcon: Construct[RawCopyObj[SubconParsedType], BuildTypes] ) -> Hex[ + RawCopyObj[SubconParsedType], + BuildTypes, HexDisplayedDict[str, t.Union[int, bytes, SubconParsedType]], BuildTypes, ]: ... @t.overload def __new__( - cls: "type[Hex[HexDisplayedDict[str, t.Any], BuildTypes]]", - subcon: Construct[Container[t.Any], BuildTypes], - ) -> Hex[HexDisplayedDict[str, t.Any], BuildTypes]: ... + cls, subcon: Construct[Container[t.Any], BuildTypes] + ) -> Hex[ + Container[t.Any], BuildTypes, HexDisplayedDict[str, t.Any], BuildTypes + ]: ... @t.overload def __new__( - cls: "type[Hex[SubconParsedType, SubconBuildTypes]]", - subcon: Construct[SubconParsedType, SubconBuildTypes], - ) -> Hex[SubconParsedType, SubconBuildTypes]: ... + cls, subcon: Construct[SubconParsedType, SubconBuildTypes] + ) -> Hex[ + SubconParsedType, SubconBuildTypes, SubconParsedType, SubconBuildTypes + ]: ... -class HexDump(Adapter[t.Any, t.Any, ParsedType, BuildTypes]): +class HexDump(Adapter[SubconParsedType, SubconBuildTypes, ParsedType, BuildTypes]): @t.overload def __new__( - cls: "type[HexDump[HexDumpDisplayedBytes, BuildTypes]]", - subcon: Construct[bytes, BuildTypes], - ) -> HexDump[HexDumpDisplayedBytes, BuildTypes]: ... + cls, subcon: Construct[bytes, BuildTypes] + ) -> HexDump[bytes, BuildTypes, HexDumpDisplayedBytes, BuildTypes]: ... @t.overload def __new__( - cls: "type[HexDump[HexDumpDisplayedDict[str, t.Union[int, bytes, SubconParsedType]],BuildTypes,]]", - subcon: Construct[RawCopyObj[SubconParsedType], BuildTypes], + cls, subcon: Construct[RawCopyObj[SubconParsedType], BuildTypes] ) -> HexDump[ + RawCopyObj[SubconParsedType], + BuildTypes, HexDumpDisplayedDict[str, t.Union[int, bytes, SubconParsedType]], BuildTypes, ]: ... @t.overload def __new__( - cls: "type[HexDump[HexDumpDisplayedDict[str, t.Any], BuildTypes]]", - subcon: Construct[Container[t.Any], BuildTypes], - ) -> HexDump[HexDumpDisplayedDict[str, t.Any], BuildTypes]: ... + cls, subcon: Construct[Container[t.Any], BuildTypes] + ) -> HexDump[ + Container[t.Any], BuildTypes, HexDumpDisplayedDict[str, t.Any], BuildTypes + ]: ... @t.overload def __new__( - cls: "type[HexDump[SubconParsedType, SubconBuildTypes]]", - subcon: Construct[SubconParsedType, SubconBuildTypes], - ) -> HexDump[SubconParsedType, SubconBuildTypes]: ... + cls, subcon: Construct[SubconParsedType, SubconBuildTypes] + ) -> HexDump[ + SubconParsedType, SubconBuildTypes, SubconParsedType, SubconBuildTypes + ]: ... # =============================================================================== # conditional @@ -748,62 +705,49 @@ class HexDump(Adapter[t.Any, t.Any, ParsedType, BuildTypes]): class Union(Construct[Container[t.Any], t.Dict[str, t.Any]]): parsefrom: t.Optional[ConstantOrContextLambda[t.Union[int, str]]] subcons: t.List[Construct[t.Any, t.Any]] - _subcons: t.Dict[str, Construct[t.Any, t.Any]] def __init__( self, parsefrom: t.Optional[ConstantOrContextLambda[t.Union[int, str]]], *subcons: Construct[t.Any, t.Any], - **subconskw: Construct[t.Any, t.Any], + **subconskw: Construct[t.Any, t.Any] ) -> None: ... def __getattr__(self, name: str) -> t.Any: ... # this can maybe made better when variadic generics are available -class Select(Construct[t.Any, t.Any]): +class Select(Construct[ParsedType, BuildTypes]): subcons: t.List[Construct[t.Any, t.Any]] - def __init__( - self, - *subcons: Construct[t.Any, t.Any], - **subconskw: Construct[t.Any, t.Any], - ) -> None: ... + def __new__( + cls, *subcons: Construct[t.Any, t.Any], **subconskw: Construct[t.Any, t.Any] + ) -> Select[t.Any, t.Any]: ... def Optional( subcon: Construct[SubconParsedType, SubconBuildTypes] -) -> Construct[t.Union[SubconParsedType, None], t.Union[SubconBuildTypes, None]]: ... +) -> Select[t.Union[SubconParsedType, None], t.Union[SubconBuildTypes, None]]: ... ThenParsedType = t.TypeVar("ThenParsedType") ThenBuildTypes = t.TypeVar("ThenBuildTypes") ElseParsedType = t.TypeVar("ElseParsedType") ElseBuildTypes = t.TypeVar("ElseBuildTypes") -class IfThenElse(Construct[ParsedType, BuildTypes]): +# This does not represent the original code, but it is the only solution that works good with pyright +class _IfThenElse(Construct[ParsedType, BuildTypes]): condfunc: ConstantOrContextLambda[bool] - thensubcon: Construct[t.Any, t.Any] - elsesubcon: Construct[t.Any, t.Any] - @t.overload - def __new__( - cls: "type[IfThenElse[t.Union[ThenParsedType, ElseParsedType], t.Union[ThenBuildTypes, ElseBuildTypes]]]", - condfunc: ConstantOrContextLambda[bool], - thensubcon: Construct[ThenParsedType, ThenBuildTypes], - elsesubcon: Construct[ElseParsedType, ElseBuildTypes], - ) -> "IfThenElse[t.Union[ThenParsedType, ElseParsedType], t.Union[ThenBuildTypes, ElseBuildTypes]]": ... - @t.overload - def __new__( - cls: "type[IfThenElse[t.Any, t.Any]]", - condfunc: ConstantOrContextLambda[bool], - thensubcon: Construct[t.Any, t.Any], - elsesubcon: Construct[t.Any, t.Any], - ) -> "IfThenElse[t.Any, t.Any]": ... + thensubcon: Construct[ParsedType, BuildTypes] + elsesubcon: Construct[ParsedType, BuildTypes] +def IfThenElse( + condfunc: ConstantOrContextLambda[bool], + thensubcon: Construct[ThenParsedType, ThenBuildTypes], + elsesubcon: Construct[ElseParsedType, ElseBuildTypes], +) -> _IfThenElse[ + t.Union[ThenParsedType, ElseParsedType], t.Union[ThenBuildTypes, ElseBuildTypes] +]: ... def If( condfunc: ConstantOrContextLambda[bool], subcon: Construct[ThenParsedType, ThenBuildTypes], -) -> IfThenElse[t.Optional[ThenParsedType], t.Optional[ThenBuildTypes]]: ... +) -> _IfThenElse[t.Union[ThenParsedType, None], t.Union[ThenBuildTypes, None]]: ... SwitchType = t.TypeVar("SwitchType") -SwitchParsedType = t.TypeVar("SwitchParsedType") -SwitchBuildTypes = t.TypeVar("SwitchBuildTypes") -SwitchDefaultParsedType = t.TypeVar("SwitchDefaultParsedType") -SwitchDefaultBuildTypes = t.TypeVar("SwitchDefaultBuildTypes") class Switch(Construct[ParsedType, BuildTypes]): keyfunc: ConstantOrContextLambda[t.Any] @@ -811,39 +755,22 @@ class Switch(Construct[ParsedType, BuildTypes]): default: Construct[t.Any, t.Any] @t.overload def __new__( - cls: "type[Switch[SwitchParsedType | None, SwitchBuildTypes | None]]", + cls, keyfunc: ConstantOrContextLambda[SwitchType], - cases: dict[t.Any, Construct[SwitchParsedType, SwitchBuildTypes]], - default: None = ..., - ) -> Switch[SwitchParsedType | None, SwitchBuildTypes | None]: ... + cases: t.Dict[SwitchType, Construct[int, int]], + default: t.Optional[Construct[int, int]] = ..., + ) -> Switch[int, t.Optional[int]]: ... @t.overload def __new__( - cls: "type[Switch[SwitchParsedType, SwitchBuildTypes]]", - keyfunc: ConstantOrContextLambda[SwitchType], - cases: dict[t.Any, Construct[SwitchParsedType, SwitchBuildTypes]], - default: Construct[t.NoReturn, t.NoReturn], - ) -> Switch[SwitchParsedType, SwitchBuildTypes]: ... - @t.overload - def __new__( - cls: "type[Switch[SwitchParsedType | SwitchDefaultParsedType, SwitchBuildTypes | SwitchDefaultBuildTypes]]", - keyfunc: ConstantOrContextLambda[SwitchType], - cases: dict[t.Any, Construct[SwitchParsedType, SwitchBuildTypes]], - default: Construct[SwitchDefaultParsedType, SwitchDefaultBuildTypes], - ) -> Switch[SwitchParsedType | SwitchDefaultParsedType, SwitchBuildTypes | SwitchDefaultBuildTypes]: ... - @t.overload - def __new__( - cls: "type[Switch[t.Any, t.Any]]", - keyfunc: ConstantOrContextLambda[SwitchType], - cases: dict[t.Any, Construct[t.Any, t.Any]], - default: Construct[t.Any, t.Any] | None = ..., + cls, + keyfunc: ConstantOrContextLambda[t.Any], + cases: t.Dict[t.Any, Construct[t.Any, t.Any]], + default: t.Optional[Construct[t.Any, t.Any]] = ..., ) -> Switch[t.Any, t.Any]: ... -class StopIf(Construct[None, None]): +class StopIf(Construct[ParsedType, BuildTypes]): condfunc: ConstantOrContextLambda[bool] - def __init__( - self, - condfunc: ConstantOrContextLambda[bool], - ) -> None: ... + def __new__(cls, condfunc: ConstantOrContextLambda[bool]) -> StopIf[None, None]: ... # =============================================================================== # alignment and padding @@ -879,8 +806,8 @@ class Aligned( def AlignedStruct( modulus: ConstantOrContextLambda[int], *subcons: Construct[t.Any, t.Any], - **subconskw: Construct[t.Any, t.Any], -) -> Struct: ... + **subconskw: Construct[t.Any, t.Any] +) -> Struct[Container[t.Any], t.Optional[t.Dict[str, t.Any]]]: ... def BitStruct( *subcons: Construct[t.Any, t.Any], **subconskw: Construct[t.Any, t.Any] ) -> t.Union[ @@ -903,28 +830,16 @@ class Pointer( stream: t.Optional[t.Callable[[Context], StreamType]] = ..., ) -> None: ... -class Peek( - Subconstruct[ +class Peek(Subconstruct[SubconParsedType, SubconBuildTypes, ParsedType, BuildTypes]): + def __new__( + cls, + subcon: Construct[SubconParsedType, SubconBuildTypes], + ) -> Peek[ SubconParsedType, SubconBuildTypes, SubconParsedType, t.Union[SubconBuildTypes, None], - ] -): - def __init__( - self, - subcon: Construct[SubconParsedType, SubconBuildTypes], - ) -> None: ... - -class OffsettedEnd( - Subconstruct[SubconParsedType, SubconBuildTypes, SubconParsedType, SubconBuildTypes] -): - endoffset: ConstantOrContextLambda[int] - def __init__( - self, - endoffset: ConstantOrContextLambda[int], - subcon: Construct[SubconParsedType, SubconBuildTypes], - ) -> None: ... + ]: ... class Seek(Construct[int, None]): at: ConstantOrContextLambda[int] @@ -954,23 +869,22 @@ class RawCopyObj(t.Generic[ParsedType], Container[t.Any]): offset2: int length: int -class RawCopy( - Subconstruct[ +class RawCopy(Subconstruct[SubconParsedType, SubconBuildTypes, ParsedType, BuildTypes]): + def __new__( + cls, subcon: Construct[SubconParsedType, SubconBuildTypes] + ) -> RawCopy[ SubconParsedType, SubconBuildTypes, RawCopyObj[SubconParsedType], t.Optional[t.Dict[str, t.Union[SubconBuildTypes, bytes]]], - ] -): - def __init__( - self, - subcon: Construct[SubconParsedType, SubconBuildTypes], - ) -> None: ... + ]: ... def ByteSwapped( subcon: Construct[SubconParsedType, SubconBuildTypes] ) -> Transformed[SubconParsedType, SubconBuildTypes]: ... -def BitsSwapped(subcon: Construct[SubconParsedType, SubconBuildTypes]) -> t.Union[ +def BitsSwapped( + subcon: Construct[SubconParsedType, SubconBuildTypes] +) -> t.Union[ Transformed[SubconParsedType, SubconBuildTypes], Restreamed[SubconParsedType, SubconBuildTypes], ]: ... @@ -993,6 +907,8 @@ def PrefixedArray( ) -> Array[ SubconParsedType, SubconBuildTypes, + ListContainer[SubconParsedType], + t.List[SubconBuildTypes], ]: ... class FixedSized( @@ -1026,9 +942,7 @@ class NullStripped( ): pad: bytes def __init__( - self, - subcon: Construct[SubconParsedType, SubconBuildTypes], - pad: bytes = ..., + self, subcon: Construct[SubconParsedType, SubconBuildTypes], pad: bytes = ... ) -> None: ... class RestreamData( @@ -1080,26 +994,26 @@ class Restreamed( ) -> None: ... class ProcessXor( - Subconstruct[SubconParsedType, SubconBuildTypes, SubconParsedType, SubconBuildTypes] + Subconstruct[SubconParsedType, SubconBuildTypes, SubconParsedType, SubconParsedType] ): padfunc: ConstantOrContextLambda2[t.Union[int, bytes]] - def __init__( - self, + def __new__( + cls, padfunc: ConstantOrContextLambda2[t.Union[int, bytes]], subcon: Construct[SubconParsedType, SubconBuildTypes], - ) -> None: ... + ) -> ProcessXor[SubconParsedType, SubconBuildTypes]: ... class ProcessRotateLeft( - Subconstruct[SubconParsedType, SubconBuildTypes, SubconParsedType, SubconBuildTypes] + Subconstruct[SubconParsedType, SubconBuildTypes, SubconParsedType, SubconParsedType] ): amount: ConstantOrContextLambda2[int] group: ConstantOrContextLambda2[int] - def __init__( - self, + def __new__( + cls, amount: ConstantOrContextLambda2[int], group: ConstantOrContextLambda2[int], subcon: Construct[SubconParsedType, SubconBuildTypes], - ) -> None: ... + ) -> ProcessRotateLeft[SubconParsedType, SubconBuildTypes]: ... T = t.TypeVar("T") @@ -1142,58 +1056,32 @@ class Rebuffered( tailcutoff: t.Optional[int] = ..., ) -> None: ... -class EncryptedSym(Tunnel[SubconParsedType, SubconBuildTypes]): - cipher: ConstantOrContextLambda2[Cipher[Mode]] - def __init__( - self, - subcon: Construct[SubconParsedType, SubconBuildTypes], - cipher: ConstantOrContextLambda2[Cipher[Mode]], - ) -> None: ... - -class EncryptedSymAead(Tunnel[SubconParsedType, SubconBuildTypes]): - cipher: ConstantOrContextLambda2[t.Union[AESGCM, AESCCM, ChaCha20Poly1305]] - nonce: ConstantOrContextLambda2[bytes] - associated_data: ConstantOrContextLambda2[bytes] - def __init__( - self, - subcon: Construct[SubconParsedType, SubconBuildTypes], - cipher: ConstantOrContextLambda2[t.Union[AESGCM, AESCCM, ChaCha20Poly1305]], - nonce: ConstantOrContextLambda2[bytes], - associated_data: ConstantOrContextLambda2[bytes] = ..., - ) -> None: ... - # =============================================================================== # lazy equivalents # =============================================================================== -class Lazy( - Subconstruct[ +class Lazy(Subconstruct[SubconParsedType, SubconBuildTypes, ParsedType, BuildTypes]): + def __new__( + cls, + subcon: Construct[SubconParsedType, SubconBuildTypes], + ) -> Lazy[ SubconParsedType, SubconBuildTypes, t.Callable[[], SubconParsedType], t.Union[t.Callable[[], SubconParsedType], SubconParsedType], - ] -): - def __init__( - self, - subcon: Construct[SubconParsedType, SubconBuildTypes], - ) -> None: ... + ]: ... class LazyContainer(t.Generic[ContainerType], t.Dict[str, ContainerType]): def __getattr__(self, name: str) -> ContainerType: ... def __getitem__(self, index: t.Union[str, int]) -> ContainerType: ... - def keys(self) -> t.Iterator[str]: ... # type: ignore - def values(self) -> t.List[ContainerType]: ... # type: ignore - def items(self) -> t.List[t.Tuple[str, ContainerType]]: ... # type: ignore + def keys(self) -> t.Iterator[str]: ... + def values(self) -> t.List[ContainerType]: ... + def items(self) -> t.List[t.Tuple[str, ContainerType]]: ... -class LazyStruct(Construct[LazyContainer[t.Any], t.Optional[t.Dict[str, t.Any]]]): +class LazyStruct(Construct[ParsedType, BuildTypes]): subcons: t.List[Construct[t.Any, t.Any]] - _subcons: t.Dict[str, Construct[t.Any, t.Any]] - _subconsindexes: t.Dict[str, int] - def __init__( - self, - *subcons: Construct[t.Any, t.Any], - **subconskw: Construct[t.Any, t.Any], - ) -> None: ... + def __new__( + cls, *subcons: Construct[t.Any, t.Any], **subconskw: Construct[t.Any, t.Any] + ) -> LazyStruct[LazyContainer[t.Any], t.Optional[t.Dict[str, t.Any]]]: ... def __getattr__(self, name: str) -> t.Any: ... class LazyListContainer(t.List[ListType]): ... @@ -1202,50 +1090,56 @@ class LazyArray( Subconstruct[ SubconParsedType, SubconBuildTypes, - ListContainer[SubconParsedType], # type: ignore - t.List[SubconBuildTypes], # type: ignore + ParsedType, + BuildTypes, ] ): count: ConstantOrContextLambda[int] - def __init__( - self, + def __new__( + cls, count: ConstantOrContextLambda[int], subcon: Construct[SubconParsedType, SubconBuildTypes], - ) -> None: ... + ) -> LazyArray[ + SubconParsedType, + SubconBuildTypes, + ListContainer[SubconParsedType], + t.List[SubconBuildTypes], + ]: ... class LazyBound(Construct[ParsedType, BuildTypes]): subconfunc: t.Callable[[], Construct[ParsedType, BuildTypes]] - def __init__( - self, - subconfunc: t.Callable[[], Construct[ParsedType, BuildTypes]], - ) -> None: ... + def __new__( + cls, subconfunc: t.Callable[[], Construct[ParsedType, BuildTypes]] + ) -> LazyBound[ParsedType, BuildTypes]: ... # =============================================================================== # adapters and validators # =============================================================================== class ExprAdapter(Adapter[SubconParsedType, SubconBuildTypes, ParsedType, BuildTypes]): - def __init__( - self, + def __new__( + cls, subcon: Construct[SubconParsedType, SubconBuildTypes], decoder: t.Callable[[SubconParsedType, Context], ParsedType], encoder: t.Callable[[BuildTypes, Context], SubconBuildTypes], - ) -> None: ... + ) -> ExprAdapter[SubconParsedType, SubconBuildTypes, ParsedType, BuildTypes]: ... class ExprSymmetricAdapter( ExprAdapter[SubconParsedType, SubconBuildTypes, ParsedType, BuildTypes] ): - def __init__( - self, + def __new__( + cls, subcon: Construct[SubconParsedType, SubconBuildTypes], encoder: t.Callable[[BuildTypes, Context], SubconBuildTypes], - ) -> None: ... + ) -> ExprSymmetricAdapter[ + SubconParsedType, SubconBuildTypes, ParsedType, BuildTypes + ]: ... class ExprValidator(Validator[SubconParsedType, SubconBuildTypes]): - def __init__( - self, + def __new__( + cls, subcon: Construct[SubconParsedType, SubconBuildTypes], validator: t.Callable[[SubconParsedType, Context], bool], - ) -> None: ... + ) -> ExprValidator[SubconParsedType, SubconBuildTypes]: ... def OneOf( subcon: Construct[SubconParsedType, SubconBuildTypes], @@ -1263,23 +1157,22 @@ def Filter( ]: ... class Slicing( - Adapter[ - SubconParsedType, - SubconBuildTypes, - ListContainer[SubconParsedType], # type: ignore - t.List[SubconBuildTypes], # type: ignore - ] + Adapter[SubconParsedType, SubconBuildTypes, SubconParsedType, SubconBuildTypes] ): - def __init__( - self, + def __new__( + cls, subcon: t.Union[ Array[ SubconParsedType, SubconBuildTypes, + ListContainer[SubconParsedType], + t.List[SubconBuildTypes], ], GreedyRange[ SubconParsedType, SubconBuildTypes, + ListContainer[SubconParsedType], + t.List[SubconBuildTypes], ], ], count: int, @@ -1287,24 +1180,28 @@ class Slicing( stop: t.Optional[int], step: int = ..., empty: t.Optional[SubconParsedType] = ..., - ) -> None: ... + ) -> Slicing[ListContainer[SubconParsedType], t.List[SubconBuildTypes]]: ... class Indexing( Adapter[SubconParsedType, SubconBuildTypes, SubconParsedType, SubconBuildTypes] ): - def __init__( - self, + def __new__( + cls, subcon: t.Union[ Array[ SubconParsedType, SubconBuildTypes, + ListContainer[SubconParsedType], + t.List[SubconBuildTypes], ], GreedyRange[ SubconParsedType, SubconBuildTypes, + ListContainer[SubconParsedType], + t.List[SubconBuildTypes], ], ], count: int, index: int, empty: t.Optional[SubconParsedType] = ..., - ) -> None: ... + ) -> Indexing[SubconParsedType, SubconBuildTypes]: ... diff --git a/construct-stubs/expr.pyi b/construct-stubs/expr.pyi index a7c1a1a..8450a44 100644 --- a/construct-stubs/expr.pyi +++ b/construct-stubs/expr.pyi @@ -1,3 +1,4 @@ +import operator import typing as t from construct.core import * @@ -469,7 +470,7 @@ class ExprMixin(t.Generic[ReturnType], object): @t.overload def __eq__(self: ExprMixin[float], other: ConstOrCallable[float]) -> BinExpr[bool]: ... @t.overload - def __eq__(self, other: ConstOrCallable[t.Any]) -> BinExpr[t.Any]: ... # type: ignore + def __eq__(self, other: t.Any) -> BinExpr[t.Any]: ... # __ne__ ########################################################################################################### @t.overload @@ -487,7 +488,7 @@ class ExprMixin(t.Generic[ReturnType], object): @t.overload def __ne__(self: ExprMixin[float], other: ConstOrCallable[float]) -> BinExpr[bool]: ... @t.overload - def __ne__(self, other: t.Any) -> BinExpr[t.Any]: ... # type: ignore + def __ne__(self, other: t.Any) -> BinExpr[t.Any]: ... # __neg__ ########################################################################################################## @t.overload @@ -497,7 +498,7 @@ class ExprMixin(t.Generic[ReturnType], object): @t.overload def __neg__(self: ExprMixin[float]) -> BinExpr[float]: ... @t.overload - def __neg__(self) -> BinExpr[t.Any]: ... + def __neg__(self) -> UniExpr[t.Any]: ... # __pos__ ########################################################################################################## @t.overload @@ -507,7 +508,7 @@ class ExprMixin(t.Generic[ReturnType], object): @t.overload def __pos__(self: ExprMixin[float]) -> BinExpr[float]: ... @t.overload - def __pos__(self) -> BinExpr[t.Any]: ... + def __pos__(self) -> UniExpr[t.Any]: ... # __invert__ ####################################################################################################### @t.overload @@ -515,7 +516,7 @@ class ExprMixin(t.Generic[ReturnType], object): @t.overload def __invert__(self: ExprMixin[bool]) -> BinExpr[int]: ... @t.overload - def __invert__(self) -> BinExpr[t.Any]: ... + def __invert__(self) -> UniExpr[t.Any]: ... # __inv__ ########################################################################################################## def __inv__(self) -> UniExpr[t.Any]: ... @@ -542,7 +543,7 @@ class Path2(ExprMixin[ReturnType]): class FuncPath(ExprMixin[ReturnType]): - def __init__(self, func: t.Callable[[t.Any], ReturnType], operand: t.Optional[t.Any] = ...) -> None: ... + def __init__(self, func: t.Callable[[t.Any], t.Any], operand: t.Optional[t.Any] = ...) -> None: ... def __call__(self, operand: t.Any, *args: t.Any) -> ReturnType: ... diff --git a/construct-stubs/lib/containers.pyi b/construct-stubs/lib/containers.pyi index 37efc75..a50033a 100644 --- a/construct-stubs/lib/containers.pyi +++ b/construct-stubs/lib/containers.pyi @@ -19,7 +19,7 @@ def recursion_lock( class Container(t.Generic[ContainerType], t.Dict[str, ContainerType]): def __getattr__(self, name: str) -> ContainerType: ... - def update( # type: ignore + def update( self, seqordict: t.Union[t.Dict[str, ContainerType], t.Tuple[str, ContainerType]], ) -> None: ... diff --git a/construct-stubs/lib/hex.pyi b/construct-stubs/lib/hex.pyi index a39d918..afa985f 100644 --- a/construct-stubs/lib/hex.pyi +++ b/construct-stubs/lib/hex.pyi @@ -1,5 +1,6 @@ import typing as t + class HexDisplayedInteger(int): ... class HexDisplayedBytes(bytes): ... @@ -9,6 +10,3 @@ V = t.TypeVar("V") class HexDisplayedDict(t.Dict[K, V]): ... class HexDumpDisplayedBytes(bytes): ... class HexDumpDisplayedDict(t.Dict[K, V]): ... - -def hexdump(data: bytes, linesize: int) -> str: ... -def hexundump(data: str, linesize: int) -> bytes: ... diff --git a/construct-stubs/lib/py3compat.pyi b/construct-stubs/lib/py3compat.pyi index c86f2f5..f105096 100644 --- a/construct-stubs/lib/py3compat.pyi +++ b/construct-stubs/lib/py3compat.pyi @@ -1,6 +1,5 @@ import typing as t -PY: t.Tuple[int, int] PY2: bool PY3: bool PYPY: bool diff --git a/construct_typed/__init__.py b/construct_typed/__init__.py index f594f2b..6db86df 100644 --- a/construct_typed/__init__.py +++ b/construct_typed/__init__.py @@ -1,55 +1,38 @@ from .dataclass_struct import ( DataclassBitStruct, - DataclassMixin, DataclassStruct, - TBitStruct, - TContainerBase, - TContainerMixin, - TStruct, - TStructField, csfield, - sfield, - EnhancedDataclassMixin ) -from .generic_wrapper import ( +from .attrs_struct import AttrsStruct, attrs_field, this_struct +from .generics import ( Adapter, ConstantOrContextLambda, - ConstantOrContextLambda2, Construct, Context, ListContainer, PathType, - Array, - Subconstruct, - Computed, + Constructable, + construct, ) -from .tenum import EnumBase, EnumValue, FlagsEnumBase, TEnum, TFlagsEnum +from .tenum import EnumBase, FlagsEnumBase, EnumConstruct, FlagsEnumConstruct __all__ = [ + "AttrsStruct", + "attrs_field", "DataclassBitStruct", - "DataclassMixin", "DataclassStruct", - "TBitStruct", - "TContainerBase", - "TContainerMixin", - "TStruct", - "TStructField", + "this_struct", "csfield", - "sfield", - "EnhancedDataclassMixin", + "Constructable", + "construct", "EnumBase", - "EnumValue", "FlagsEnumBase", - "TEnum", - "TFlagsEnum", + "EnumConstruct", + "FlagsEnumConstruct", "Adapter", "ConstantOrContextLambda", - "ConstantOrContextLambda2", "Construct", "Context", "ListContainer", "PathType", - "Array", - "Subconstruct", - "Computed" ] diff --git a/construct_typed/attrs_struct.py b/construct_typed/attrs_struct.py new file mode 100644 index 0000000..f2778e9 --- /dev/null +++ b/construct_typed/attrs_struct.py @@ -0,0 +1,237 @@ +# -*- coding: utf-8 -*- +# pyright: strict +import textwrap +import typing as t + +import attr +import construct as cs +from .generics import Adapter, Construct, Context, ParsedType, PathType + +T = t.TypeVar("T") + +# 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 + + +ATTRS_METADATA_KEY = "__construct_typed_subcon" + + +def attrs_field( + subcon: Construct[ParsedType, t.Any], + doc: t.Optional[str] = None, + parsed: t.Optional[t.Callable[[t.Any, Context], None]] = None, +) -> ParsedType: + """ + Helper method for `AttrsStruct` and `AttrsBitStruct` to create the attrs fields. + + This method also processes `Const` and `Default`, to pass these values als default values to the dataclass. + + # TODO: Implement `default` parameter for `attrs_field` + """ + 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 = attr.NOTHING + + # 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, + attr.field( + default=default, + init=init, + metadata={ATTRS_METADATA_KEY: subcon}, + ), + ) + + +class AttrsConstruct(Adapter[t.Any, t.Any, T, T]): + if t.TYPE_CHECKING: + + def __new__( + cls, + attrs_cls: t.Type[T], + reverse_fields: bool = False, + ) -> "AttrsConstruct[T]": + ... + + def __init__( + self, + attrs_cls: t.Type[T], + reverse_fields: bool = False, + ) -> None: + if not attr.has(attrs_cls): + raise TypeError(f"'{attrs_cls}' has to be a 'attrs' object") + + self.attrs_cls = attrs_cls + self.reverse_fields = reverse_fields + + # get all fields from the dataclass + fields = attr.fields(attrs_cls) + if reverse_fields: + fields = tuple(reversed(fields)) + + # extract the construct formats from the struct_type + subcon_fields = {} + for field in fields: + subcon_fields[field.name] = field.metadata[ATTRS_METADATA_KEY] + + # init adatper + super().__init__(cs.Struct(**subcon_fields)) # type: ignore + + def _decode( + self, + obj: "cs.Container[t.Any]", + context: Context, + path: PathType, + ) -> T: + # get all fields from the dataclass + fields = attr.fields(self.attrs_cls) + + # 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.attrs_cls(**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.attrs_cls): + raise TypeError(f"'{repr(obj)}' has to be of type {repr(self.attrs_cls)}") + + # get all fields from the dataclass + fields = attr.fields(self.attrs_cls) + + # 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__(kw_only_default=True, field_descriptors=(attrs_field,)) +class AttrsStruct: + """ + Adapter for a attrs-class 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. + + Metaclass paramters:: + + :param constr: Create a more complex construct. `this_struct` can be used for representing this AttrsStruct object. + :param reverse_fields: Flag if the fields should be reversed parsed/build + + Example:: + + >>> from construct import Bytes, Int8ub, this + >>> from construct_typed import AttrsStruct, attrs_field, construct + >>> class Image(AttrsStruct): + ... width: int = attrs_field(Int8ub) + ... height: int = attrs_field(Int8ub) + ... pixels: bytes = attrs_field(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 attrs class + cls = attr.define(cls, kw_only=True, slots=False) + + # create construct format + attrs_constr = AttrsConstruct(cls, reverse_fields) + if constr is this_struct: + constr = attrs_constr + else: + _replace_this_struct(constr, attrs_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) + + def __setitem__(self, key: str, value: t.Any) -> None: + setattr(self, key, value) + + if t.TYPE_CHECKING: + + @classmethod + def __construct__(cls: t.Type[T]) -> "AttrsConstruct[T]": + ... diff --git a/construct_typed/dataclass_struct.py b/construct_typed/dataclass_struct.py index e626085..b5ea258 100644 --- a/construct_typed/dataclass_struct.py +++ b/construct_typed/dataclass_struct.py @@ -1,6 +1,5 @@ # -*- coding: utf-8 -*- # pyright: strict -# pyright: reportIncompatibleVariableOverride=false, reportAny=false import dataclasses import textwrap import typing as t @@ -12,25 +11,250 @@ from construct.lib.containers import ( recursion_lock, ) from construct.lib.py3compat import bytestringtype, reprstring, unicodestringtype -from typing_extensions import override -from .generic_wrapper import Adapter, Construct, Context, ParsedType, PathType +from construct_typed.generics 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]): + """ + 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, construct + >>> @dataclasses.dataclass + ... 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') """ - __dataclass_fields__: "t.ClassVar[dict[str, dataclasses.Field[t.Any]]]" + 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 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: + r""" + TODO: Add Documentation + """ + + @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 attrs class + 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) @@ -76,209 +300,42 @@ class DataclassMixin: text.append(indentation.join(str(v).split("\n"))) return "".join(text) + if t.TYPE_CHECKING: -def csfield( - subcon: Construct[ParsedType, t.Any], - doc: str | None = None, - parsed: t.Callable[[t.Any, Context], None] | 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]" = orig_subcon - default = const_subcon.value - elif isinstance(orig_subcon, cs.Default): - default_subcon: "cs.Default[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}, - ), - ) + @classmethod + def __construct__(cls: t.Type[T]) -> "DataclassConstruct[T]": + ... -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" # type: ignore - def __init__( - self, - dc_type: type[DataclassType], - reverse: bool = False, - ) -> None: - self.dc_type: type[DataclassType] = dc_type - self.reverse: bool = 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: dict[str, t.Any] = {} - 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) - - @override - 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 # type: ignore - - @override - def _encode( - self, obj: DataclassType, context: Context, path: PathType - ) -> 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: 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: type[DataclassType], reverse: bool = False -) -> "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)) - -class EnhancedDataclassMixin(DataclassMixin): - @classmethod - def format(cls): - return DataclassStruct(cls) @classmethod - def build(cls, obj: t.Self, **kw: dict[str, t.Any]): - return cls.format().build(obj, **kw) - - @classmethod - def parse(cls, data: bytes | bytearray, **kw: dict[str, t.Any]): - return cls.format().parse(data, **kw) - - @classmethod - def parse_file(cls, file: str, **kw: dict[str, t.Any]): - return cls.format().parse_file(file, **kw) - - @classmethod - def parse_stream(cls, stream: t.IO[bytes], **kw: dict[str, t.Any]): - return cls.format().parse_stream(stream, **kw) - - def build_self(self) -> bytes: - return self.build(self) - -# support legacy names -TStruct = DataclassStruct -TBitStruct = DataclassBitStruct -TContainerMixin = DataclassMixin -TContainerBase = DataclassMixin -TStructField = csfield -sfield = csfield + 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 diff --git a/construct_typed/generic_wrapper.py b/construct_typed/generics.py similarity index 69% rename from construct_typed/generic_wrapper.py rename to construct_typed/generics.py index cd4788b..1bb647d 100644 --- a/construct_typed/generic_wrapper.py +++ b/construct_typed/generics.py @@ -12,14 +12,11 @@ if t.TYPE_CHECKING: # while type checking, the original classes are already generics, because they are defined like this in the stubs. from construct import Adapter as Adapter from construct import ConstantOrContextLambda as ConstantOrContextLambda - from construct import ConstantOrContextLambda2 as ConstantOrContextLambda2 from construct import Construct as Construct from construct import Context as Context from construct import ListContainer as ListContainer from construct import PathType as PathType - from construct import Array as Array - from construct import Subconstruct as Subconstruct - from construct import Computed as Computed + else: import construct as cs @@ -40,18 +37,22 @@ else: class Context: pass - class Array( - t.Generic[SubconParsedType, SubconBuildTypes], - cs.Array, - ): - pass - - class Subconstruct(t.Generic[SubconParsedType, SubconBuildTypes, ParsedType, BuildTypes], cs.Subconstruct): - pass - - class Computed(t.Generic[ParsedType], cs.Computed): - pass - ConstantOrContextLambda = t.Union[ValueType, t.Callable[[Context], t.Any]] - ConstantOrContextLambda2 = t.Union[ValueType, t.Callable[[Context], ValueType]] 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 \ No newline at end of file diff --git a/construct_typed/tenum.py b/construct_typed/tenum.py index 4417c6b..37eba0c 100644 --- a/construct_typed/tenum.py +++ b/construct_typed/tenum.py @@ -1,111 +1,101 @@ -# pyright: reportAny=false import enum import typing as t -from typing_extensions import Self, override +import construct as cs -from .generic_wrapper import Construct, Adapter, Context, PathType - - -# ## TEnum ############################################################################################################ -class EnumValue: - """ - This is a helper class for adding documentation to an enum value. - """ - - def __init__(self, value: int, doc: str | None = None) -> None: - self.value: int = value - self.__doc__ = doc if doc else "" +from .generics import * + +T = t.TypeVar("T") +# ## EnumConstruct ############################################################################################################ class EnumBase(enum.IntEnum): """ - Base class for an Enum used in `construct_typed.TEnum`. + Base class for an Enum used in `construct_typed.EnumConstruct`. - This class extends the standard `enum.IntEnum` by. - - missing values are automatically generated - - possibility to add documentation for each enum value (see `EnumValue`) - - Example:: - - >>> class State(EnumBase): - ... Idle = 1 - ... Running = EnumValue(2, "This is the running state.") - - >>> State(1) - - - >>> State["Idle"] - - - >>> State.Idle - - - >>> State(3) # missing value - - - >>> State.Running.__doc__ # documentation - 'This is the running state.' + This class extends the standard `enum.IntEnum`, so that missing values are automatically generated. """ - def __new__(cls, val: EnumValue | int) -> "Self": - if isinstance(val, EnumValue): - obj = int.__new__(cls, val.value) - obj._value_ = val.value - obj.__doc__ = val.__doc__ - else: - obj = int.__new__(cls, val) - obj._value_ = val - obj.__doc__ = "" - return obj + @classmethod + def __init_subclass__( + cls, + subcon: "cs.Construct[t.Any, t.Any]", + **kwargs: t.Any, + ): + super().__init_subclass__(**kwargs) - # Extend the enum type with _missing_ method. So if a enum value + # 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 = EnumConstruct(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 - @override - def _missing_(cls, value: t.Any) -> enum.Enum | None: + def _missing_(cls, value: t.Any) -> t.Optional["EnumBase"]: if isinstance(value, int): - pseudo_member = cls._value2member_map_.get(value, None) - if pseudo_member is None: - new_member = int.__new__(cls, value) - # I expect a name attribute to hold a string, hence str(value) - # However, new_member._name_ = value works, too - new_member._name_ = str(value) - new_member._value_ = value - new_member.__doc__ = "missing value" - pseudo_member = cls._value2member_map_.setdefault(value, new_member) - return pseudo_member + return cls._create_pseudo_member_(value) return None # will raise the ValueError in Enum.__new__ - @override - def __reduce_ex__(self, proto: t.Any) -> tuple[t.Any, ...]: - """ - Pickle enums by value instead of name (restores pre-3.11 behavior). - See https://github.com/python/cpython/pull/26658 for why this exists. - """ - return self.__class__, (self._value_,) + @classmethod + def _create_pseudo_member_(cls, value: int) -> "EnumBase": + pseudo_member = cls._value2member_map_.get(value, None) # type: ignore + if pseudo_member is None: + new_member = int.__new__(cls, value) + # I expect a name attribute to hold a string, hence str(value) + # However, new_member._name_ = value works, too + new_member._name_ = str(value) + new_member._value_ = value + 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]") -> "EnumConstruct[EnumType]": + ... EnumType = t.TypeVar("EnumType", bound=EnumBase) -class TEnum(Adapter[int, int, EnumType, EnumType]): +class EnumConstruct(Adapter[int, int, EnumType, EnumType]): """ Typed enum. """ - def __init__(self, subcon: Construct[int, int], enum_type: type[EnumType]): + + if t.TYPE_CHECKING: + + def __new__( + cls, subcon: Construct[int, int], enum_type: t.Type[EnumType] + ) -> "EnumConstruct[EnumType]": + ... + + def __init__(self, subcon: Construct[int, int], enum_type: t.Type[EnumType]): + if not issubclass(enum_type, EnumBase): + raise TypeError( + "'{}' has to be a '{}'".format(repr(enum_type), repr(EnumBase)) + ) + # save enum type - self.enum_type: type[EnumType] = enum_type + self.enum_type = t.cast(t.Type[EnumType], enum_type) # type: ignore # init adatper - super(TEnum, self).__init__(subcon) # type: ignore + super(EnumConstruct, self).__init__(subcon) # type: ignore - @override def _decode(self, obj: int, context: Context, path: PathType) -> EnumType: return self.enum_type(obj) - @override def _encode( self, obj: EnumType, @@ -119,88 +109,69 @@ class TEnum(Adapter[int, int, EnumType, EnumType]): ) -# ## TFlagsEnum ####################################################################################################### +# ## FlagsEnumConstruct ####################################################################################################### class FlagsEnumBase(enum.IntFlag): - """ - Base class for an Enum used in `construct_typed.TFlagsEnum`. - - This class extends the standard `enum.IntFlag` by. - - possibility to add documentation for each enum value (see `EnumValue`) - - Example:: - - >>> class Option(FlagsEnumBase): - ... OptOne = 1 - ... OptTwo = EnumValue(2, "This is option two.") - - >>> Option(1) - - - >>> Option["OptOne"] - - - >>> Option.OptOne - - - >>> Option(3) - - - >>> Option(4) - - - >>> Option.OptTwo.__doc__ # documentation - 'This is option two.' - """ - - def __new__(cls, val: EnumValue | int) -> "Self": - if isinstance(val, EnumValue): - obj = int.__new__(cls, val.value) - obj._value_ = val.value - obj.__doc__ = val.__doc__ - else: - obj = int.__new__(cls, val) - obj._value_ = val - obj.__doc__ = "" - return obj - @classmethod - @override - def _missing_(cls, value: t.Any) -> t.Any: - """ - Returns member (possibly creating it) if one can be found for value. - """ - new_member = super()._missing_(value) - new_member.__doc__ = "missing value" - return new_member + def __init_subclass__( + cls, + subcon: "cs.Construct[t.Any, t.Any]", + **kwargs: t.Any, + ): + super().__init_subclass__(**kwargs) - @override - def __reduce_ex__(self, proto: t.Any) -> tuple[t.Any, ...]: - """ - Pickle enums by value instead of name (restores pre-3.11 behavior). - See https://github.com/python/cpython/pull/26658 for why this exists. - """ - return self.__class__, (self._value_,) + # 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 = FlagsEnumConstruct(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]", + ) -> "FlagsEnumConstruct[FlagsEnumType]": + ... FlagsEnumType = t.TypeVar("FlagsEnumType", bound=FlagsEnumBase) -class TFlagsEnum(Adapter[int, int, FlagsEnumType, FlagsEnumType]): +class FlagsEnumConstruct(Adapter[int, int, FlagsEnumType, FlagsEnumType]): """ Typed enum. """ - def __init__(self, subcon: Construct[int, int], enum_type: type[FlagsEnumType]): + + if t.TYPE_CHECKING: + + def __new__( + cls, subcon: Construct[int, int], enum_type: t.Type[FlagsEnumType] + ) -> "FlagsEnumConstruct[FlagsEnumType]": + ... + + def __init__(self, subcon: Construct[int, int], enum_type: t.Type[FlagsEnumType]): + if not issubclass(enum_type, FlagsEnumBase): + raise TypeError( + "'{}' has to be a '{}'".format(repr(enum_type), repr(FlagsEnumBase)) + ) + # save enum type - self.enum_type: type[FlagsEnumType] = enum_type + self.enum_type = t.cast(t.Type[FlagsEnumType], enum_type) # type: ignore # init adatper - super(TFlagsEnum, self).__init__(subcon) # type: ignore + super(FlagsEnumConstruct, self).__init__(subcon) # type: ignore - @override def _decode(self, obj: int, context: Context, path: PathType) -> FlagsEnumType: return self.enum_type(obj) - @override def _encode( self, obj: FlagsEnumType, diff --git a/construct_typed/version.py b/construct_typed/version.py index 38a2845..1a555cf 100644 --- a/construct_typed/version.py +++ b/construct_typed/version.py @@ -1,2 +1,2 @@ -version = (0, 7, 0) -version_string = "0.7.0+wrapper" +version = (0, 5, 2) +version_string = "0.5.2" \ No newline at end of file diff --git a/mypy.ini b/mypy.ini new file mode 100644 index 0000000..3412486 --- /dev/null +++ b/mypy.ini @@ -0,0 +1,3 @@ +[mypy] +strict = True +warn_unused_ignores = False \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml deleted file mode 100644 index 8a2689b..0000000 --- a/pyproject.toml +++ /dev/null @@ -1,76 +0,0 @@ - -[build-system] -requires = ["setuptools >= 75.8.0"] -build-backend = "setuptools.build_meta" - -[project] -name="construct-typing" -dynamic = ["version"] -license = { file = "LICENSE" } -description="Extension for the python package 'construct' that adds typing features" -readme = "README.md" -authors=[{ name = "Tim Riddermann" }] -requires-python = ">=3.9" -dependencies = [ - "construct==2.10.70", - "typing_extensions>=4.6.0" -] -keywords = [ - "construct", - "kaitai", - "declarative", - "data structure", - "struct", - "binary", - "symmetric", - "parser", - "builder", - "parsing", - "building", - "pack", - "unpack", - "packer", - "unpacker", - "bitstring", - "bytestring", - "annotation", - "type hint", - "typing", - "typed", - "bitstruct", - "PEP 561", -] -classifiers = [ - "Development Status :: 3 - Alpha", - "License :: OSI Approved :: MIT License", - "Intended Audience :: Developers", - "Topic :: Software Development :: Libraries :: Python Modules", - "Topic :: Software Development :: Build Tools", - "Topic :: Software Development :: Code Generators", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.11", - "Programming Language :: Python :: 3.12", - "Programming Language :: Python :: 3.13", - "Programming Language :: Python :: Implementation :: CPython", - "Typing :: Typed", -] - -[project.urls] -"Homepage" = "https://github.com/timrid/construct-typing" -"Bug Reports" = "https://github.com/timrid/construct-typing/issues" - -[tool.setuptools] -packages=[ - "construct-stubs", - "construct-stubs.lib", - "construct_typed" -] - -[tool.setuptools.dynamic] -version = {attr = "construct_typed.version.version_string"} - -[tool.mypy] -strict = true -warn_unused_ignores = false \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 2514c06..2d70b89 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,14 +1,10 @@ -construct==2.10.70 +construct==2.10.67 pytest>=6.2.0 -numpy +numpy==1.21.* arrow ruamel.yaml cloudpickle lz4 black isort -mypy -cryptography -build -setuptools -wheel +mypy \ No newline at end of file diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..6cdeb7a --- /dev/null +++ b/setup.py @@ -0,0 +1,64 @@ +#!/usr/bin/env python +from setuptools import setup + +version_string = "?.?.?" +exec(open("./construct_typed/version.py").read()) + +setup( + name="construct-typing", + version=version_string, + packages=["construct-stubs", "construct_typed"], + package_data={ + "construct-stubs": ["*.pyi", "lib/*.pyi"], + "construct_typed": ["py.typed"], + }, + license="MIT", + license_files=("LICENSE",), + description="Extension for the python package 'construct' that adds typing features", + long_description=open("README.md").read(), + long_description_content_type="text/markdown", + platforms=["POSIX", "Windows"], + url="https://github.com/timrid/construct-typing", + author="Tim Riddermann", + python_requires=">=3.7", + install_requires=["construct==2.10.67"], + keywords=[ + "construct", + "kaitai", + "declarative", + "data structure", + "struct", + "binary", + "symmetric", + "parser", + "builder", + "parsing", + "building", + "pack", + "unpack", + "packer", + "unpacker", + "bitstring", + "bytestring", + "annotation", + "type hint", + "typing", + "typed", + "bitstruct", + "PEP 561", + ], + classifiers=[ + "Development Status :: 3 - Alpha", + "License :: OSI Approved :: MIT License", + "Intended Audience :: Developers", + "Topic :: Software Development :: Libraries :: Python Modules", + "Topic :: Software Development :: Build Tools", + "Topic :: Software Development :: Code Generators", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: Implementation :: CPython", + "Typing :: Typed", + ], +) diff --git a/tests/declarativeunittest.py b/tests/declarativeunittest.py index ed7fb59..1d1be0c 100644 --- a/tests/declarativeunittest.py +++ b/tests/declarativeunittest.py @@ -1,170 +1,38 @@ -import binascii -import io -import typing as t - import pytest -from construct import * -from construct.lib import * - -import construct_typed as cst xfail = pytest.mark.xfail skip = pytest.mark.skip skipif = pytest.mark.skipif -Buffer = t.Union[bytes, memoryview, bytearray] -ParsedType = t.TypeVar("ParsedType") -BuildTypes = t.TypeVar("BuildTypes") -ContainerType = t.TypeVar("ContainerType", bound=cst.TContainerMixin) -T = t.TypeVar("T") +import os, math, random, collections, itertools, io, hashlib, binascii -IdentType = t.TypeVar("IdentType") +from construct import * +from construct.lib import * class ZeroIO(io.BufferedIOBase): - def read(self, __size: t.Optional[int] = None) -> bytes: + def read(self, __size=None): if __size is not None: return bytes(__size) else: return bytes(0) - def read1(self, __size: int = 0) -> bytes: + def read1(self, __size=0): return bytes(__size) -def ident(x: IdentType) -> IdentType: - return x +ident = lambda x: x +devzero = ZeroIO() -devzero: t.BinaryIO = ZeroIO() # type: ignore - - -def raises( - func: t.Callable[..., t.Any], *args: t.Any, **kw: t.Any -) -> t.Union[t.Any, Exception]: +def raises(func, *args, **kw): try: return func(*args, **kw) except Exception as e: return e.__class__ -@t.overload -def common( - format: cst.TStruct[ContainerType], - datasample: Buffer, - objsample: t.Union[ContainerType, t.Dict[str, t.Any]], - sizesample: t.Union[int, t.Type[Exception]] = ..., - **kw: t.Any -) -> None: - ... - - -@t.overload -def common( - format: "Construct[ListContainer[ParsedType], t.Any]", - datasample: Buffer, - objsample: t.List[ParsedType], - sizesample: t.Union[int, t.Type[Exception]] = ..., - **kw: t.Any -) -> None: - ... - - -@t.overload -def common( - format: "Construct[Container[t.Any], t.Any]", - datasample: Buffer, - objsample: t.Dict[str, t.Any], - sizesample: t.Union[int, t.Type[Exception]] = ..., - **kw: t.Any -) -> None: - ... - - -@t.overload -def common( - format: "Construct[t.Union[EnumInteger, EnumIntegerString], t.Any]", - datasample: Buffer, - objsample: t.Union[int, str], - sizesample: t.Union[int, t.Type[Exception]] = ..., - **kw: t.Any -) -> None: - ... - - -@t.overload -def common( - format: "Construct[HexDisplayedInteger, t.Any]", - datasample: Buffer, - objsample: t.Union[HexDisplayedInteger, int], - sizesample: t.Union[int, t.Type[Exception]] = ..., - **kw: t.Any -) -> None: - ... - - -@t.overload -def common( - format: "Construct[HexDisplayedBytes, t.Any]", - datasample: Buffer, - objsample: t.Union[HexDisplayedBytes, bytes], - sizesample: t.Union[int, t.Type[Exception]] = ..., - **kw: t.Any -) -> None: - ... - - -@t.overload -def common( - format: "Construct[HexDisplayedDict[str, t.Any], t.Any]", - datasample: Buffer, - objsample: t.Dict[str, t.Any], - sizesample: t.Union[int, t.Type[Exception]] = ..., - **kw: t.Any -) -> None: - ... - - -@t.overload -def common( - format: "Construct[HexDumpDisplayedBytes, t.Any]", - datasample: Buffer, - objsample: t.Union[HexDumpDisplayedBytes, bytes], - sizesample: t.Union[int, t.Type[Exception]] = ..., - **kw: t.Any -) -> None: - ... - - -@t.overload -def common( - format: "Construct[HexDumpDisplayedDict[str, t.Any], t.Any]", - datasample: Buffer, - objsample: t.Dict[str, t.Any], - sizesample: t.Union[int, t.Type[Exception]] = ..., - **kw: t.Any -) -> None: - ... - - -@t.overload -def common( - format: "Construct[ParsedType, t.Any]", - datasample: Buffer, - objsample: ParsedType, - sizesample: t.Union[int, t.Type[Exception]] = ..., - **kw: t.Any -) -> None: - ... - - -def common( - format: "Construct[t.Any, t.Any]", - datasample: Buffer, - objsample: t.Any, - sizesample: t.Union[int, t.Type[Exception]] = SizeofError, - **kw: t.Any -) -> None: +def common(format, datasample, objsample, sizesample=SizeofError, **kw): obj = format.parse(datasample, **kw) assert obj == objsample data = format.build(objsample, **kw) @@ -176,35 +44,35 @@ def common( size = format.sizeof(**kw) assert size == sizesample else: - size_ex = raises(format.sizeof, **kw) - assert size_ex == sizesample + size = raises(format.sizeof, **kw) + assert size == sizesample -def setattrs(obj: T, **kwargs: t.Any) -> T: - """Set multiple named values of an object""" +def setattrs(obj, **kwargs): + """ Set multiple named values of an object """ for name, value in kwargs.items(): setattr(obj, name, value) return obj -def commonhex(format: "Construct[t.Any, t.Any]", hexdata: str) -> None: +def commonhex(format, hexdata): commonbytes(format, binascii.unhexlify(hexdata)) -def commondumpdeprecated(format: "Construct[t.Any, t.Any]", filename: str) -> None: +def commondumpdeprecated(format, filename): filename = "tests/deprecated_gallery/blobs/" + filename with open(filename, "rb") as f: data = f.read() commonbytes(format, data) -def commondump(format: "Construct[t.Any, t.Any]", filename: str) -> None: +def commondump(format, filename): filename = "tests/gallery/blobs/" + filename with open(filename, "rb") as f: data = f.read() commonbytes(format, data) -def commonbytes(format: "Construct[t.Any, t.Any]", data: bytes) -> None: +def commonbytes(format, data): obj = format.parse(data) - format.build(obj) + data2 = format.build(obj) diff --git a/tests/declarativeunittest.pyi b/tests/declarativeunittest.pyi new file mode 100644 index 0000000..e2f8cab --- /dev/null +++ b/tests/declarativeunittest.pyi @@ -0,0 +1,109 @@ +import typing as t +from construct import * +from construct.lib import * +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) +T = t.TypeVar("T") + +IdentType = t.TypeVar("IdentType") + +def ident(p1: IdentType) -> IdentType: ... + +devzero: t.BinaryIO + +def raises( + func: t.Callable[..., t.Any], *args: t.Any, **kw: t.Any +) -> t.Union[t.Any, Exception]: ... +@t.overload +def common( + format: cst.TStruct[ContainerType], + datasample: Buffer, + objsample: t.Union[ContainerType, t.Dict[str, t.Any]], + sizesample: t.Union[int, t.Type[Exception]] = ..., + **kw: t.Any +) -> None: ... +@t.overload +def common( + format: Construct[ListContainer[ParsedType], t.Any], + datasample: Buffer, + objsample: t.List[ParsedType], + sizesample: t.Union[int, t.Type[Exception]] = ..., + **kw: t.Any +) -> None: ... +@t.overload +def common( + format: Construct[Container[t.Any], t.Any], + datasample: Buffer, + objsample: t.Dict[str, t.Any], + sizesample: t.Union[int, t.Type[Exception]] = ..., + **kw: t.Any +) -> None: ... +@t.overload +def common( + format: Construct[t.Union[EnumInteger, EnumIntegerString], t.Any], + datasample: Buffer, + objsample: t.Union[int, str], + sizesample: t.Union[int, t.Type[Exception]] = ..., + **kw: t.Any +) -> None: ... +@t.overload +def common( + format: Construct[HexDisplayedInteger, t.Any], + datasample: Buffer, + objsample: t.Union[HexDisplayedInteger, int], + sizesample: t.Union[int, t.Type[Exception]] = ..., + **kw: t.Any +) -> None: ... +@t.overload +def common( + format: Construct[HexDisplayedBytes, t.Any], + datasample: Buffer, + objsample: t.Union[HexDisplayedBytes, bytes], + sizesample: t.Union[int, t.Type[Exception]] = ..., + **kw: t.Any +) -> None: ... +@t.overload +def common( + format: Construct[HexDisplayedDict[str, t.Any], t.Any], + datasample: Buffer, + objsample: t.Dict[str, t.Any], + sizesample: t.Union[int, t.Type[Exception]] = ..., + **kw: t.Any +) -> None: ... +@t.overload +def common( + format: Construct[HexDumpDisplayedBytes, t.Any], + datasample: Buffer, + objsample: t.Union[HexDumpDisplayedBytes, bytes], + sizesample: t.Union[int, t.Type[Exception]] = ..., + **kw: t.Any +) -> None: ... +@t.overload +def common( + format: Construct[HexDumpDisplayedDict[str, t.Any], t.Any], + datasample: Buffer, + objsample: t.Dict[str, t.Any], + sizesample: t.Union[int, t.Type[Exception]] = ..., + **kw: t.Any +) -> None: ... +@t.overload +def common( + format: Construct[ParsedType, t.Any], + datasample: Buffer, + objsample: ParsedType, + sizesample: t.Union[int, t.Type[Exception]] = ..., + **kw: t.Any +) -> None: ... +def setattrs(obj: T, **kwargs: t.Any) -> T: ... +def commonhex(format: Construct[t.Any, t.Any], hexdata: str) -> None: ... +def commondumpdeprecated( + format: Construct[t.Any, t.Any], filename: str +) -> None: ... +def commondump(format: Construct[t.Any, t.Any], filename: str) -> None: ... +def commonbytes( + format: Construct[ParsedType, t.Any], data: ParsedType +) -> None: ... diff --git a/tests/test_core.py b/tests/test_core.py index 602a899..6f65772 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -# mypy: no-warn-unused-ignores -from .declarativeunittest import raises, common, ident, devzero + +from .declarativeunittest import raises, common, commonhex, commondumpdeprecated, commondump, commonbytes, ident, devzero from construct.core import * from construct import * from construct.lib import * @@ -151,29 +151,17 @@ def test_formatfield_bool_issue_901() -> None: assert d.sizeof() == 1 def test_bytesinteger() -> None: - d = BytesInteger(0) - assert raises(d.parse, b"") == IntegerError - assert raises(d.build, 0) == IntegerError d = BytesInteger(4, signed=True, swapped=False) common(d, b"\x01\x02\x03\x04", 0x01020304, 4) common(d, b"\xff\xff\xff\xff", -1, 4) d = BytesInteger(4, signed=False, swapped=this.swapped) common(d, b"\x01\x02\x03\x04", 0x01020304, 4, swapped=False) common(d, b"\x04\x03\x02\x01", 0x01020304, 4, swapped=True) - assert raises(BytesInteger(-1).parse, b"") == IntegerError - assert raises(BytesInteger(-1).build, 0) == IntegerError - assert raises(BytesInteger(8).build, None) == IntegerError - assert raises(BytesInteger(8, signed=False).build, -1) == IntegerError - assert raises(BytesInteger(8, True).build, -2**64) == IntegerError - assert raises(BytesInteger(8, True).build, 2**64) == IntegerError - assert raises(BytesInteger(8, False).build, -2**64) == IntegerError - assert raises(BytesInteger(8, False).build, 2**64) == IntegerError assert raises(BytesInteger(this.missing).sizeof) == SizeofError + assert raises(BytesInteger(4, signed=False).build, -1) == IntegerError + common(BytesInteger(0), b"", 0, 0) def test_bitsinteger() -> None: - d = BitsInteger(0) - assert raises(d.parse, b"") == IntegerError - assert raises(d.build, 0) == IntegerError d = BitsInteger(8) common(d, b"\x01\x01\x01\x01\x01\x01\x01\x01", 255, 8) d = BitsInteger(8, signed=True) @@ -183,17 +171,9 @@ def test_bitsinteger() -> None: d = BitsInteger(16, swapped=this.swapped) common(d, b"\x01\x01\x01\x01\x01\x01\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00", 0xff00, 16, swapped=False) common(d, b"\x00\x00\x00\x00\x00\x00\x00\x00\x01\x01\x01\x01\x01\x01\x01\x01", 0xff00, 16, swapped=True) - assert raises(BitsInteger(-1).parse, b"") == IntegerError - assert raises(BitsInteger(-1).build, 0) == IntegerError - assert raises(BitsInteger(5, swapped=True).parse, bytes(5)) == IntegerError - assert raises(BitsInteger(5, swapped=True).build, 0) == IntegerError - assert raises(BitsInteger(8).build, None) == IntegerError - assert raises(BitsInteger(8, signed=False).build, -1) == IntegerError - assert raises(BitsInteger(8, True).build, -2**64) == IntegerError - assert raises(BitsInteger(8, True).build, 2**64) == IntegerError - assert raises(BitsInteger(8, False).build, -2**64) == IntegerError - assert raises(BitsInteger(8, False).build, 2**64) == IntegerError assert raises(BitsInteger(this.missing).sizeof) == SizeofError + assert raises(BitsInteger(8, signed=False).build, -1) == IntegerError + common(BitsInteger(0), b"", 0, 0) def test_varint() -> None: d = VarInt @@ -244,8 +224,8 @@ def test_paddedstring() -> None: common(PaddedString(100, e), data, s, 100) for e in ["ascii","utf8","utf16","utf-16-le","utf32","utf-32-le"]: - assert PaddedString(10, e).sizeof() == 10 - assert PaddedString(this.n, e).sizeof(n=10) == 10 + PaddedString(10, e).sizeof() == 10 + PaddedString(this.n, e).sizeof(n=10) == 10 def test_pascalstring() -> None: for e,_ in [("utf8",1),("utf16",2),("utf_16_le",2),("utf32",4),("utf_32_le",4)]: @@ -256,8 +236,8 @@ def test_pascalstring() -> None: common(PascalString(sc, e), sc.build(0), u"") for e in ["utf8","utf16","utf-16-le","utf32","utf-32-le","ascii"]: - assert raises(PascalString(Byte, e).sizeof) == SizeofError - assert raises(PascalString(VarInt, e).sizeof) == SizeofError + raises(PascalString(Byte, e).sizeof) == SizeofError + raises(PascalString(VarInt, e).sizeof) == SizeofError def test_cstring() -> None: s = u"" @@ -266,12 +246,12 @@ def test_cstring() -> None: common(CString(e), s.encode(e)+bytes(us), s) common(CString(e), bytes(us), u"") - assert CString("utf8").build(s) == b'\xd0\x90\xd1\x84\xd0\xbe\xd0\xbd'+b"\x00" - assert CString("utf16").build(s) == b'\xff\xfe\x10\x04D\x04>\x04=\x04'+b"\x00\x00" - assert CString("utf32").build(s) == b'\xff\xfe\x00\x00\x10\x04\x00\x00D\x04\x00\x00>\x04\x00\x00=\x04\x00\x00'+b"\x00\x00\x00\x00" + CString("utf8").build(s) == b'\xd0\x90\xd1\x84\xd0\xbe\xd0\xbd'+b"\x00" + CString("utf16").build(s) == b'\xff\xfe\x10\x04D\x04>\x04=\x04'+b"\x00\x00" + CString("utf32").build(s) == b'\xff\xfe\x00\x00\x10\x04\x00\x00D\x04\x00\x00>\x04\x00\x00=\x04\x00\x00'+b"\x00\x00\x00\x00" for e in ["utf8","utf16","utf-16-le","utf32","utf-32-le","ascii"]: - assert raises(CString(e).sizeof) == SizeofError + raises(CString(e).sizeof) == SizeofError def test_greedystring() -> None: for e,_ in [("utf8",1),("utf16",2),("utf_16_le",2),("utf32",4),("utf_32_le",4)]: @@ -280,7 +260,7 @@ def test_greedystring() -> None: common(GreedyString(e), b"", u"") for e in ["utf8","utf16","utf-16-le","utf32","utf-32-le","ascii"]: - assert raises(GreedyString(e).sizeof) == SizeofError + raises(GreedyString(e).sizeof) == SizeofError def test_string_encodings() -> None: # checks that "-" is replaced with "_" @@ -291,7 +271,7 @@ def test_flag() -> None: d = Flag common(d, b"\x00", False, 1) common(d, b"\x01", True, 1) - assert d.parse(b"\xff") == True + d.parse(b"\xff") == True def test_enum() -> None: d = Enum(Byte, one=1, two=2, four=4, eight=8) @@ -440,11 +420,11 @@ def test_struct_proper_context() -> None: "x"/Byte, "inner"/Struct( "y"/Byte, - "a"/Computed(this._.x+1), # type: ignore - "b"/Computed(this.y+2), # type: ignore + "a"/Computed(this._.x+1), + "b"/Computed(this.y+2), ), - "c"/Computed(this.x+3), # type: ignore - "d"/Computed(this.inner.y+4), # type: ignore + "c"/Computed(this.x+3), + "d"/Computed(this.inner.y+4), ) assert d.parse(b"\x01\x0f") == Container(x=1, inner=Container(y=15, a=2, b=17), c=4, d=19) @@ -531,7 +511,7 @@ def test_const() -> None: def test_computed() -> None: common(Computed(255), b"", 255, 0) - common(Computed(lambda ctx: 255), b"", 255, 0) # type: ignore + common(Computed(lambda ctx: 255), b"", 255, 0) assert Computed(255).build(None) == b"" assert Struct(Computed(255)).build({}) == b"" assert raises(Computed(this.missing).parse, b"") == KeyError @@ -611,7 +591,7 @@ def test_rebuild_issue_664() -> None: def test_default() -> None: d = Default(Byte, 0) common(d, b"\xff", 255, 1) - assert d.build(None) == b"\x00" + d.build(None) == b"\x00" def test_check() -> None: common(Check(True), b"", None, 0) @@ -657,7 +637,8 @@ def test_numpy_error() -> None: numpy.load(io.BytesIO(b"")) # type: ignore def test_namedtuple() -> None: - coord = t.NamedTuple("coord", [("x", int), ("y", int), ("z", int)]) + import collections + coord = collections.namedtuple("coord", "x y z") d1 = NamedTuple("coord", "x y z", Array(3, Byte)) common(d1, b"123", coord(49,50,51), 3) d2 = NamedTuple("coord", "x y z", GreedyRange(Byte)) @@ -727,13 +708,10 @@ def test_hexdump() -> None: def test_hexdump_regression_issue_188() -> None: # Hex HexDump were not inheriting subcon flags - a = Hex(Const(b"MZ")) - d = Struct(a) + d = Struct(Hex(Const(b"MZ"))) assert d.parse(b"MZ") == Container() assert d.build(dict()) == b"MZ" - - b = HexDump(Const(b"MZ")) - d = Struct(b) + d = Struct(HexDump(Const(b"MZ"))) assert d.parse(b"MZ") == Container() assert d.build(dict()) == b"MZ" @@ -830,10 +808,8 @@ def test_select_buildfromnone_issue_747() -> None: assert d.build(dict()) == b"" def test_if() -> None: - d = If(True, Byte) - common(d, b"\x01", 1, 1) - d = If(False, Byte) - common(d, b"", None, 0) + common(If(True, Byte), b"\x01", 1, 1) + common(If(False, Byte), b"", None, 0) def test_ifthenelse() -> None: common(IfThenElse(True, Int8ub, Int16ub), b"\x01", 1, 1) @@ -946,17 +922,6 @@ def test_peek() -> None: assert d4.build(Container(a=0x01, b=0x0102)) == b"" assert d4.sizeof() == 0 -def test_offsettedend() -> None: - d1 = Struct( - "header" / Bytes(2), - "data" / OffsettedEnd(-2, GreedyBytes), - "footer" / Bytes(2), - ) - common(d1, b"\x01\x02\x03\x04\x05\x06\x07", Container(header=b'\x01\x02', data=b'\x03\x04\x05', footer=b'\x06\x07')) - - d2 = OffsettedEnd(0, Byte) - assert raises(d2.sizeof) == SizeofError - def test_seek() -> None: d = Seek(5) assert d.parse(b"") == 5 @@ -1068,14 +1033,13 @@ def test_prefixed() -> None: common(d5, b"\x0a"+bytes(10), u"\x00"*10, SizeofError) def test_prefixedarray() -> None: - d = PrefixedArray(Byte, Byte) - common(d, b"\x02\x0a\x0b", [10,11], SizeofError) - assert d.parse(b"\x03\x01\x02\x03") == [1,2,3] - assert d.parse(b"\x00") == [] - assert d.build([1,2,3]) == b"\x03\x01\x02\x03" - assert raises(d.parse, b"") == StreamError - assert raises(d.parse, b"\x03\x01") == StreamError - assert raises(d.sizeof) == SizeofError + common(PrefixedArray(Byte,Byte), b"\x02\x0a\x0b", [10,11], SizeofError) + assert PrefixedArray(Byte, Byte).parse(b"\x03\x01\x02\x03") == [1,2,3] + assert PrefixedArray(Byte, Byte).parse(b"\x00") == [] + assert PrefixedArray(Byte, Byte).build([1,2,3]) == b"\x03\x01\x02\x03" + assert raises(PrefixedArray(Byte, Byte).parse, b"") == StreamError + assert raises(PrefixedArray(Byte, Byte).parse, b"\x03\x01") == StreamError + assert raises(PrefixedArray(Byte, Byte).sizeof) == SizeofError def test_fixedsized() -> None: d1 = FixedSized(10, Byte) @@ -1249,7 +1213,7 @@ def test_checksum() -> None: def test_checksum_nonbytes_issue_323() -> None: d = Struct( "vals" / Byte[2], - "checksum" / Checksum(Byte, lambda vals: int(sum(vals)) & 0xFF, this.vals), + "checksum" / Checksum(Byte, lambda vals: sum(vals) & 0xFF, this.vals), ) assert d.parse(b"\x00\x00\x00") == Container(vals=[0, 0], checksum=0) assert raises(d.parse, b"\x00\x00\x01") == ChecksumError @@ -1365,105 +1329,6 @@ def test_compressed_prefixed() -> None: assert st.parse(st.build(Container(one=zeros,two=zeros))) == Container(one=zeros,two=zeros) assert raises(d.sizeof) == SizeofError -@pytest.mark.xfail(ONWINDOWS and PYPY, reason="no wheel for 'cryptography' is currently available for pypy on windows") -def test_encryptedsym() -> None: - from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes - key128 = b"\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f" - key256 = b"\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f" - iv = b"\x20\x21\x22\x23\x24\x25\x26\x27\x28\x29\x2a\x2b\x2c\x2d\x2e\x2f" - nonce = iv - - # AES 128/256 bit - ECB - d = EncryptedSym(GreedyBytes, lambda ctx: Cipher(algorithms.AES(ctx.key), modes.ECB())) - common(d, b"\xf4\x0f\x54\xb7\x6a\x7a\xf1\xdb\x92\x73\x14\xde\x2f\xa0\x3e\x2d", b'Secret Message..', key=key128, iv=iv) - common(d, b"\x82\x6b\x01\x82\x90\x02\xa1\x9e\x35\x0a\xe2\xc3\xee\x1a\x42\xf5", b'Secret Message..', key=key256, iv=iv) - - # AES 128/256 bit - CBC - d = EncryptedSym(GreedyBytes, lambda ctx: Cipher(algorithms.AES(ctx.key), modes.CBC(ctx.iv))) - common(d, b"\xba\x79\xc2\x62\x22\x08\x29\xb9\xfb\xd3\x90\xc4\x04\xb7\x55\x87", b'Secret Message..', key=key128, iv=iv) - common(d, b"\x60\xc2\x45\x0d\x7e\x41\xd4\xf8\x85\xd4\x8a\x64\xd1\x45\x49\xe3", b'Secret Message..', key=key256, iv=iv) - - # AES 128/256 bit - CTR - d = EncryptedSym(GreedyBytes, lambda ctx: Cipher(algorithms.AES(ctx.key), modes.CTR(ctx.nonce))) - common(d, b"\x80\x78\xb6\x0c\x07\xf5\x0c\x90\xce\xa2\xbf\xcb\x5b\x22\xb9\xb5", b'Secret Message..', key=key128, nonce=nonce) - common(d, b"\x6a\xae\x7b\x86\x1a\xa6\xe0\x6a\x49\x02\x02\x1b\xf2\x3c\xd8\x0d", b'Secret Message..', key=key256, nonce=nonce) - - assert raises(EncryptedSym(GreedyBytes, "AES").build, b"") == CipherError # type: ignore - assert raises(EncryptedSym(GreedyBytes, "AES").parse, b"") == CipherError # type: ignore - -@pytest.mark.xfail(ONWINDOWS and PYPY, reason="no wheel for 'cryptography' is currently available for pypy on windows") -def test_encryptedsym_cbc_example() -> None: - from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes - d = Struct( - "iv" / Default(Bytes(16), os.urandom(16)), - "enc_data" / EncryptedSym( - Aligned(16, - Struct( - "width" / Int16ul, - "height" / Int16ul - ) - ), - lambda ctx: Cipher(algorithms.AES(ctx._.key), modes.CBC(ctx.iv)) - ) - ) - key128 = b"\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f" - byts = d.build({"enc_data": {"width": 5, "height": 4}}, key=key128) - obj = d.parse(byts, key=key128) - assert obj.enc_data == Container(width=5, height=4) - -@pytest.mark.xfail(ONWINDOWS and PYPY, reason="no wheel for 'cryptography' is currently available for pypy on windows") -def test_encryptedsymaead() -> None: - from cryptography.hazmat.primitives.ciphers import aead - key128 = b"\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f" - key256 = b"\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f" - nonce = b"\x20\x21\x22\x23\x24\x25\x26\x27\x28\x29\x2a\x2b\x2c\x2d\x2e\x2f" - - # AES 128/256 bit - GCM - d = Struct( - "associated_data" / Bytes(21), - "data" / EncryptedSymAead( - GreedyBytes, - lambda ctx: aead.AESGCM(ctx._.key), - this._.nonce, - this.associated_data - ) - ) - common( - d, - b"This is authenticated\xb6\xd3\x64\x0c\x7a\x31\xaa\x16\xa3\x58\xec\x17\x39\x99\x2e\xf8\x4e\x41\x17\x76\x3f\xd1\x06\x47\x04\x9f\x42\x1c\xf4\xa9\xfd\x99\x9c\xe9", - Container(associated_data=b"This is authenticated", data=b"The secret message"), - key=key128, - nonce=nonce - ) - common( - d, - b"This is authenticated\xde\xb4\x41\x79\xc8\x7f\xea\x8d\x0e\x41\xf6\x44\x2f\x93\x21\xe6\x37\xd1\xd3\x29\xa4\x97\xc3\xb5\xf4\x81\x72\xa1\x7f\x3b\x9b\x53\x24\xe4", - Container(associated_data=b"This is authenticated", data=b"The secret message"), - key=key256, - nonce=nonce - ) - assert raises(EncryptedSymAead(GreedyBytes, "AESGCM", bytes(16)).build, b"") == CipherError # type: ignore - assert raises(EncryptedSymAead(GreedyBytes, "AESGCM", bytes(16)).parse, b"") == CipherError # type: ignore - -@pytest.mark.xfail(ONWINDOWS and PYPY, reason="no wheel for 'cryptography' is currently available for pypy on windows") -def test_encryptedsymaead_gcm_example() -> None: - from cryptography.hazmat.primitives.ciphers import aead - d = Struct( - "nonce" / Default(Bytes(16), os.urandom(16)), - "associated_data" / Bytes(21), - "enc_data" / EncryptedSymAead( - GreedyBytes, - lambda ctx: aead.AESGCM(ctx._.key), - this.nonce, - this.associated_data - ) - ) - key128 = b"\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f" - byts = d.build({"associated_data": b"This is authenticated", "enc_data": b"The secret message"}, key=key128) - obj = d.parse(byts, key=key128) - assert obj.enc_data == b"The secret message" - assert obj.associated_data == b"This is authenticated" - def test_rebuffered() -> None: data = b"0" * 1000 assert Rebuffered(Array(1000,Byte)).parse_stream(io.BytesIO(data)) == [48]*1000 @@ -1679,7 +1544,7 @@ def test_operators() -> None: assert d.docs == "description" d = "description" * Byte assert d.docs == "description" - _ = """ + """ description """ * \ Byte @@ -1821,11 +1686,9 @@ def test_from_issue_244() -> None: assert d.parse(b"abcd") == [Container(num=97, index=0),Container(num=98, index=1),Container(num=99, index=2),Container(num=100, index=3),] def test_from_issue_269() -> None: - a = If(this.enabled, Padding(2)) - d = Struct("enabled" / Byte, a) + d = Struct("enabled" / Byte, If(this.enabled, Padding(2))) assert d.build(dict(enabled=1)) == b"\x01\x00\x00" assert d.build(dict(enabled=0)) == b"\x00" - d = Struct("enabled" / Byte, "pad" / If(this.enabled, Padding(2))) assert d.build(dict(enabled=1)) == b"\x01\x00\x00" assert d.build(dict(enabled=0)) == b"\x00" @@ -1841,7 +1704,7 @@ def test_from_issue_324() -> None: )), "checksum" / Checksum( Byte, - lambda data: int(sum(data)) & 0xFF, + lambda data: sum(data) & 0xFF, this.vals.data ), ) @@ -1932,11 +1795,11 @@ def test_pickling_constructs() -> None: ) data = bytes(100) - du = cloudpickle.loads(cloudpickle.dumps(d, protocol=-1)) # type: ignore + du = cloudpickle.loads(cloudpickle.dumps(d, protocol=-1)) assert du.parse(data) == d.parse(data) def test_pickling_constructs_issue_894() -> None: - import cloudpickle # type: ignore + import cloudpickle fundus_header = Struct( 'width' / Int32un, @@ -1948,7 +1811,7 @@ def test_pickling_constructs_issue_894() -> None: 'img' / Int8un, ) - cloudpickle.dumps(fundus_header) # type: ignore + cloudpickle.dumps(fundus_header) def test_exposing_members_attributes() -> None: d1 = Struct( @@ -2159,7 +2022,7 @@ def test_struct_root_topmost() -> None: assert d.parse(b"", z=2) == Container(x=1, inner=Container(inner2=Container(x=1,z=2,zz=2))) def test_parsedhook_repeatersdiscard() -> None: - outputs: t.List[int] = [] + outputs = [] def printobj1(obj: int, ctx: "Context") -> None: outputs.append(obj) d1 = GreedyRange(Byte * printobj1, discard=True) diff --git a/tests/test_typed.py b/tests/test_typed.py index df8cc84..b65a51c 100644 --- a/tests/test_typed.py +++ b/tests/test_typed.py @@ -1,21 +1,22 @@ # -*- coding: utf-8 -*- # pyright: strict -import dataclasses import enum -import textwrap import typing as t - +import pytest import construct as cs - import construct_typed as cst -from construct_typed import DataclassBitStruct, DataclassMixin, DataclassStruct, csfield +from construct_typed import ( + DataclassBitStruct, + DataclassStruct, + csfield, + construct, +) 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)) @@ -31,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) @@ -52,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) @@ -75,78 +74,192 @@ def test_dataclass_str_repr() -> None: ) -def test_dataclass_ifthenelse() -> None: - @dataclasses.dataclass - class IfThenElseTest(DataclassMixin): - test_if: t.Optional[int] = csfield(cs.If(False, cs.Int8ub)) - test_ifthenelse: t.Optional[int] = csfield( - cs.IfThenElse(True, cs.Int8ub, cs.Pass) - ) - - a = IfThenElseTest(test_if=None, test_ifthenelse=None) - assert a.test_if == None - assert a.test_ifthenelse == 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__() # TODO: construct(Image) assert c.width.name == "width" assert c.height.name == "height" assert c.width.subcon is cs.Int8ub assert c.height.subcon is cs.Int8ub +def test_attrs() -> None: + import attr + + @attr.s(kw_only=True) + class TestAttrs: + a: int = attr.ib() + b: int = attr.ib(default=5) + c: int = attr.ib() + + testattrs1 = TestAttrs(a=5, c=10) + print(testattrs1) + + +import construct_typed.attrs_struct as cst5 + + +def test_attrs_struct_example() -> None: + from construct import Bytes, Int8ub, this + from construct_typed import AttrsStruct, attrs_field, construct + + class Image(AttrsStruct): + width: int = attrs_field(Int8ub) + height: int = attrs_field(Int8ub) + pixels: bytes = attrs_field(Bytes(this.height * this.width)) + + d = construct(Image) + obj = d.parse(b"\x01\x0212") + assert obj.width is obj["width"] + assert obj.height is obj["height"] + assert obj.pixels is obj["pixels"] + + +def test_attrs_struct() -> None: + class Test(cst5.AttrsStruct): + a: int = cst5.attrs_field(cs.Byte) + b: int = cst5.attrs_field(cs.Byte) + c: int = cst5.attrs_field(cs.Byte) + d: int = cst5.attrs_field(cs.Byte) + + common( + cst.construct(Test), + b"\x00\x01\x02\x03", + Test(a=0, b=1, c=2, d=3), + 4, + ) + + +def test_attrs_struct_to_str() -> None: + class Test(cst5.AttrsStruct): + a: int = cst5.attrs_field(cs.Byte) + b: int = cst5.attrs_field(cs.Byte) + c: int = cst5.attrs_field(cs.Byte) + d: int = cst5.attrs_field(cs.Byte) + + obj = Test(a=0, b=1, c=2, d=3) + assert str(obj) == "Test(a=0, b=1, c=2, d=3)" + + +def test_attrs_struct_simple_constr() -> None: + class Test(cst5.AttrsStruct, constr=cst5.this_struct): + a: int = cst5.attrs_field(cs.Byte) + b: int = cst5.attrs_field(cs.Byte) + c: int = cst5.attrs_field(cs.Byte) + d: int = cst5.attrs_field(cs.Byte) + + common( + cst.construct(Test), + b"\x00\x01\x02\x03", + Test(a=0, b=1, c=2, d=3), + 4, + ) + + +def test_attrs_struct_complex_constr() -> None: + class Test(cst5.AttrsStruct, constr=cs.Bitwise(cst5.this_struct)): + a: int = cst5.attrs_field(cs.BitsInteger(2)) + b: int = cst5.attrs_field(cs.BitsInteger(2)) + c: int = cst5.attrs_field(cs.BitsInteger(2)) + d: int = cst5.attrs_field(cs.BitsInteger(2)) + + common( + cst.construct(Test), + b"\x1b", + Test(a=0, b=1, c=2, d=3), + 1, + ) + + +def test_attrs_struct_overloaded_attributes() -> None: + class Test(cst5.AttrsStruct): + a: int = cst5.attrs_field(cs.Byte) + b: int = cst5.attrs_field(cs.Byte) + subcon: int = cst5.attrs_field( + cs.Byte + ) # this is also an attribute from Construct + docs: int = cst5.attrs_field( + cs.Byte + ) # this is also an attribute from Construct + + common( + cst.construct(Test), + b"\x00\x01\x02\x03", + Test(a=0, b=1, subcon=2, docs=3), + 4, + ) + + +def test_attrs_struct_reverse_fields() -> None: + class Test(cst5.AttrsStruct, reverse_fields=True): + a: int = cst5.attrs_field(cs.Byte) + b: int = cst5.attrs_field(cs.Byte) + c: int = cst5.attrs_field(cs.Byte) + d: int = cst5.attrs_field(cs.Byte) + + common( + cst.construct(Test), + b"\x03\x02\x01\x00", + Test(a=0, b=1, c=2, d=3), + 4, + ) + + +def test_attrs_struct_unsupported_param() -> None: + with pytest.raises(TypeError): + + class Test(cst5.AttrsStruct, strange_parameter=True): # type: ignore + a: int = cst5.attrs_field(cs.Byte) + + +@pytest.mark.skip +def test_attrs_default() -> None: + # TODO: Implement `default` parameter for `attrs_field` + raise NotImplementedError + + def test_dataclass_struct_reverse() -> None: - @dataclasses.dataclass - class TestContainer(DataclassMixin): + class TestContainerReverse(DataclassStruct, reverse_fields=True): a: int = csfield(cs.Int16ub) b: int = csfield(cs.Int8ub) common( - DataclassStruct(TestContainer, reverse=True), + cst.construct(TestContainerReverse), b"\x02\x00\x01", - TestContainer(a=1, b=2), + TestContainerReverse(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(cst.construct(InnerDataclass)) common( - DataclassStruct(TestContainer), + cst.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( @@ -157,7 +270,7 @@ def test_dataclass_struct_default_field() -> None: ) common( - DataclassStruct(Image), + cst.construct(Image), b"\x02\x03\x00\x00\x00\x00\x00\x00", setattrs(Image(2, 3), pixels=bytes(6)), sample_building=Image(2, 3), @@ -165,12 +278,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), + cst.construct(TestContainer), bytes(1), setattrs(TestContainer(), const_field=b"\x00"), 1, @@ -178,7 +290,7 @@ def test_dataclass_struct_const_field() -> None: assert ( raises( - DataclassStruct(TestContainer).build, + cst.construct(TestContainer).build, setattrs(TestContainer(), const_field=b"\x01"), ) == cs.ConstError @@ -186,12 +298,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), + cst.construct(TestContainer), bytes(5), TestContainer(array_field=[0, 0, 0, 0, 0]), 5, @@ -199,15 +310,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), + cst.construct(TestContainer), bytes(2), setattrs(TestContainer(), _1=b"\x00"), cs.SizeofError, @@ -215,22 +325,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 = cst.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) @@ -246,7 +354,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( @@ -283,44 +391,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" @@ -333,7 +419,7 @@ def test_dataclass_struct_doc() -> None: """, ) - format = DataclassStruct(TestContainer) + format = TestContainer.__construct__() # TODO: construct(TestContainer) 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" @@ -345,39 +431,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(cst.EnumBase, subcon=cs.Byte): one = 1 two = 2 four = 4 eight = 8 - d = cst.TEnum(cs.Byte, TestEnum) + d = cst.construct(TestEnum) common(d, b"\x01", TestEnum.one, 1) common(d, b"\xff", TestEnum(255), 1) @@ -396,114 +479,46 @@ def test_tenum_no_enumbase() -> None: b = 2 cls = t.cast(t.Type[cst.EnumBase], E) - assert raises(lambda: cst.TEnum(cs.Byte, cls)) == TypeError + assert raises(lambda: cst.EnumConstruct(cs.Byte, cls)) == TypeError -def test_tenum_asdict() -> None: - # see: https://github.com/timrid/construct-typing/issues/21 - import dataclasses +def test_tenum_no_subcon() -> None: + with pytest.raises(TypeError): - import construct_typed as cst - - class TestEnum(cst.EnumBase): - one = 1 - two = 2 - four = 4 - eight = 8 - - @dataclasses.dataclass - class SomeDataclass: - a: TestEnum - - dc = SomeDataclass(TestEnum.one) - dc_dict = dataclasses.asdict(dc) - assert dc_dict["a"] == dc.a - assert dc_dict["a"] is dc.a - - dc = SomeDataclass(TestEnum(5)) - dc_dict = dataclasses.asdict(dc) - assert dc_dict["a"] == dc.a - assert dc_dict["a"] is dc.a - - -def test_tenum_docstring() -> None: - class TestEnum(cst.EnumBase): - """ - This is an test enum. - """ - - Value_WithDoc = cst.EnumValue(0, doc="an enum with a documentation") - Value_WithMultilineDoc = cst.EnumValue( - 1, - """ - An enum with a multiline documentation... - ...next line... - """, - ) - Value_NoDoc = cst.EnumValue(2) - Value_NoDoc2 = 3 - - assert TestEnum.__doc__ is not None - assert textwrap.dedent(TestEnum.__doc__) == textwrap.dedent( - """ - This is an test enum. - """ - ) - assert TestEnum.Value_WithDoc.__doc__ == "an enum with a documentation" - assert ( - TestEnum.Value_WithMultilineDoc.__doc__ - == """ - An enum with a multiline documentation... - ...next line... - """ - ) - assert TestEnum.Value_NoDoc.__doc__ == "" - assert TestEnum.Value_NoDoc2.__doc__ == "" - assert TestEnum(5).__doc__ == "missing value" - - -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 + class E1(cst.EnumBase): # type: ignore + a = 1 + b = 2 def test_tenum_in_tstruct() -> None: - class TestEnum(cst.EnumBase): + class TestEnum(cst.EnumBase, subcon=cs.Int8ub): a = 1 b = 2 - @dataclasses.dataclass - class TestContainer(DataclassMixin): - a: TestEnum = csfield(cst.TEnum(cs.Int8ub, TestEnum)) + class TestContainer(DataclassStruct): + a: TestEnum = csfield(cst.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(cst.construct(TestEnum).build, TestContainer(a=1, b=2)) == TypeError # type: ignore ) def test_tenum_flags() -> None: - class TestEnum(cst.FlagsEnumBase): + class TestEnum(cst.FlagsEnumBase, subcon=cs.Byte): one = 1 two = 2 four = 4 eight = 8 - d = cst.TFlagsEnum(cs.Byte, TestEnum) + d = cst.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" @@ -512,65 +527,3 @@ def test_tenum_flags() -> None: assert d.build(TestEnum(255)) == b"\xff" assert d.build(TestEnum.eight) == b"\x08" assert raises(d.build, 2) == TypeError - - -def test_tenum_flags_asdict() -> None: - import dataclasses - - import construct_typed as cst - - class TestEnum(cst.FlagsEnumBase): - one = 1 - two = 2 - four = 4 - eight = 8 - - @dataclasses.dataclass - class SomeDataclass: - a: TestEnum - - dc = SomeDataclass(TestEnum.one) - dc_dict = dataclasses.asdict(dc) - assert dc_dict["a"] == dc.a - assert dc_dict["a"] is dc.a - - dc = SomeDataclass(TestEnum(5)) - dc_dict = dataclasses.asdict(dc) - assert dc_dict["a"] == dc.a - assert dc_dict["a"] is dc.a - - -def test_tenum_flags_docstring() -> None: - class TestEnum(cst.FlagsEnumBase): - """ - This is an test flags enum. - """ - - Value_WithDoc = cst.EnumValue(0, doc="an enum with a documentation") - Value_WithMultilineDoc = cst.EnumValue( - 1, - """ - An enum with a multiline documentation... - ...next line... - """, - ) - Value_NoDoc = cst.EnumValue(2) - Value_NoDoc2 = 4 - - assert TestEnum.__doc__ is not None - assert textwrap.dedent(TestEnum.__doc__) == textwrap.dedent( - """ - This is an test flags enum. - """ - ) - assert TestEnum.Value_WithDoc.__doc__ == "an enum with a documentation" - assert ( - TestEnum.Value_WithMultilineDoc.__doc__ - == """ - An enum with a multiline documentation... - ...next line... - """ - ) - assert TestEnum.Value_NoDoc.__doc__ == "" - assert TestEnum.Value_NoDoc2.__doc__ == "" - assert TestEnum(8).__doc__ == "missing value"