From 8733327578674dde12bdcfa42a091d24be73a71e Mon Sep 17 00:00:00 2001 From: Diego Argueta Date: Wed, 13 Mar 2024 07:28:25 -0700 Subject: [PATCH] 0.11.4 (#37) * Add "no cover" pragmas where needed * Remove deprecated code/behavior, bump up coverage * Upgrade test dependencies * Typing bugfixes and such * Read package metadata for version * Bugfixes and tests * Bump version * Fix broken Tox stuff? * More bugfixes * Stricter linting, start cleanup * Revert some crashes back to warnings * Forgot to export `dataclass` decorator * Fix last of the linting * Fix accidental raise of a warning * Break circular import, fix test expecting crash * Fix some linting directives * 0.12.0 -> 0.11.4 * Upgrade dependencies * Update copyright year * Update changelog * Upgrade another test dependency * Add 3.12 to supported version badge * Fix version number after checkout * Fix test for __set__ behavior on 3.12 * Lint * Upgrade GHA versions --- .github/workflows/ci.yml | 6 +- LICENSE.txt | 4 +- README.rst | 2 +- binobj/__init__.py | 4 +- binobj/errors.py | 9 +- binobj/fields/base.py | 70 ++-- binobj/fields/containers.py | 20 +- binobj/fields/stringlike.py | 2 +- binobj/pep526.py | 10 +- binobj/structures.py | 4 +- binobj/varints.py | 10 +- docs/source/CHANGELOG.rst | 27 +- poetry.lock | 537 +++++++++++++++++++++++++++++-- pyproject.toml | 10 +- tests/conftest.py | 10 - tests/decorators_test.py | 2 +- tests/errors_test.py | 12 +- tests/fields/base_test.py | 58 +++- tests/fields/containers_test.py | 62 +++- tests/fields/numeric_test.py | 32 +- tests/fields/stringlike_test.py | 26 +- tests/full_examples/cpio_test.py | 8 +- tests/full_examples/fat_test.py | 2 +- tests/helpers_test.py | 8 +- tests/pep526_test.py | 11 +- tests/serialization_test.py | 16 +- tests/structures_test.py | 15 +- tests/varints_test.py | 43 +-- tox.ini | 4 +- 29 files changed, 814 insertions(+), 210 deletions(-) delete mode 100644 tests/conftest.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ab9fc2d6..16bcf886 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,6 +19,8 @@ jobs: tox-env: py310 - python-version: "3.11" tox-env: py311 + - python-version: "3.12" + tox-env: py312 - python-version: "pypy-3.7" tox-env: pypy37 - python-version: "pypy-3.8" @@ -32,9 +34,9 @@ jobs: - python-version: "3.11" tox-env: lint steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4.1.1 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4.7.0 + uses: actions/setup-python@v5.0.0 with: python-version: ${{ matrix.python-version }} - name: Pre-installation requirements diff --git a/LICENSE.txt b/LICENSE.txt index 0951ba8c..3cf5c65a 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,6 +1,4 @@ -BSD 3-Clause License - -Copyright (c) 2017-2023, Diego Argueta +Copyright (c) 2017-2024, Diego Argueta All rights reserved. Redistribution and use in source and binary forms, with or without diff --git a/README.rst b/README.rst index cc6b05cf..ea4f3e7f 100644 --- a/README.rst +++ b/README.rst @@ -6,7 +6,7 @@ binobj .. |build-status| image:: https://github.com/dargueta/binobj/actions/workflows/ci.yml/badge.svg :alt: Build status -.. |python-versions| image:: https://img.shields.io/badge/python-3.7,%203.8,%203.9,%203.10,%203.11-blue.svg +.. |python-versions| image:: https://img.shields.io/badge/python-3.7,%203.8,%203.9,%203.10,%203.11,%203.12-blue.svg :alt: Python versions .. |installs-month| image:: https://pepy.tech/badge/binobj/month diff --git a/binobj/__init__.py b/binobj/__init__.py index ff55a229..4ad4e9ec 100644 --- a/binobj/__init__.py +++ b/binobj/__init__.py @@ -3,7 +3,7 @@ from typing import Optional from typing import Tuple -import pkg_resources as _pkgr +import pkg_resources as _pkgr # noqa: I900 from .errors import * from .fields import * @@ -19,5 +19,5 @@ def __to_version_info() -> Tuple[int, int, int, Optional[str]]: # Do not modify directly; use ``bumpversion`` command instead. -__version__ = "0.10.5" +__version__ = "0.11.4" __version_info__ = __to_version_info() diff --git a/binobj/errors.py b/binobj/errors.py index a608f0ec..e5f5f1e6 100644 --- a/binobj/errors.py +++ b/binobj/errors.py @@ -51,7 +51,7 @@ class Error(Exception): def __init__(self, message: Optional[str] = None, *args: Any): # If there is no error message, use the first line of the docstring. - if message is None and self.__doc__: + if message is None and hasattr(self, "__doc__") and self.__doc__: message = self.__doc__.splitlines()[0] super().__init__(message, *args) @@ -232,10 +232,9 @@ class ImmutableFieldError(IllegalOperationError): """ def __init__(self, *, field: Optional["Field[Any]"] = None): + message: Optional[str] if field is not None: - message = ( - "Cannot assign to immutable field: %r" % field - ) # type: Optional[str] + message = "Cannot assign to immutable field: %r" % field else: message = None @@ -338,7 +337,7 @@ def __init__(self, *, field: "Field[Any]"): super().__init__( "Passing `DEFAULT` for `null_value` of unsized field %r makes it impossible" " to determine what None should be and would result in unpredictable" - " behavior." % self, + " behavior." % field.name, field=field, ) diff --git a/binobj/fields/base.py b/binobj/fields/base.py index 6cefbefa..84fc5721 100644 --- a/binobj/fields/base.py +++ b/binobj/fields/base.py @@ -15,7 +15,6 @@ from typing import FrozenSet from typing import Generic from typing import Iterable -from typing import Mapping from typing import Optional from typing import overload from typing import Type @@ -209,7 +208,9 @@ class Field(Generic[T]): """ __overrideable_attributes__: ClassVar[Collection[str]] = () - """The names of attributes that can be overridden using ``Meta`` class options.""" + """The names of attributes that can be configured using the containing struct's + ``Meta`` class options. + """ __explicit_init_args__: FrozenSet[str] """The names of arguments that were explicitly passed to the constructor.""" @@ -249,6 +250,8 @@ class Field(Generic[T]): _default: Union[T, None, _Undefined] """The default dump value for the field if the user doesn't pass a value in.""" + _compute_fn: Optional[Callable[["Field[T]", StrDict], Optional[T]]] # noqa: TAE002 + def __new__(cls: Type["Field[T]"], *_args: Any, **kwargs: Any) -> "Field[T]": """Create a new instance, recording which keyword arguments were passed in. @@ -289,8 +292,7 @@ def __init__( ) if default is UNDEFINED and const is not UNDEFINED: - # If no default is given but ``const`` is, set the default value to - # ``const``. + # If no default is given but `const` is, set the default value to `const`. self._default = const elif callable(default): warnings.warn( @@ -308,7 +310,7 @@ def __init__( self.name = typing.cast(str, name) self.index = typing.cast(int, None) self.offset: Optional[int] = None - self._compute_fn: Optional[Callable[["Field[T]", StrDict], Optional[T]]] = None + self._compute_fn = None if size is not None or const is UNDEFINED: self._size = size @@ -361,14 +363,7 @@ def bind_to_container( self.index = index self.offset = offset - overrideables: Mapping[str, str] - if not isinstance(self.__overrideable_attributes__, Mapping): - # Force `overrideables` to be a dictionary. - overrideables = {n: n for n in self.__overrideable_attributes__} - else: - overrideables = self.__overrideable_attributes__ - - for argument_name, attribute_name in overrideables.items(): + for argument_name in self.__overrideable_attributes__: if argument_name in self.__explicit_init_args__: # This argument was passed in to the constructor directly and any # defaults specified by the struct's metainformation should be ignored. @@ -390,13 +385,13 @@ def bind_to_container( # Found a type-specific default value setattr( self, - attribute_name, + argument_name, struct_info.argument_defaults[typed_default_name], ) elif argument_name in struct_info.argument_defaults: # Found a generic default value setattr( - self, attribute_name, struct_info.argument_defaults[argument_name] + self, argument_name, struct_info.argument_defaults[argument_name] ) # Else: struct doesn't define a default value for this argument. @@ -465,9 +460,6 @@ def compute_value_for_dump( def computes(self, method: Callable[["Field[T]", StrDict], Optional[T]]) -> None: """Decorator that marks a function as computing the value for a field. - .. deprecated:: 0.6.0 - This decorator will be moved to :mod:`binobj.decorators`. - You can use this for automatically assigning values based on other fields. For example, suppose we have this struct:: @@ -509,11 +501,6 @@ def _assign_n_numbers(self, all_fields): "Cannot set compute function for a const field.", field=self ) - warnings.warn( - "This decorator will be moved to the `decorators` module.", - DeprecationWarning, - stacklevel=2, - ) self._compute_fn = method @property @@ -547,7 +534,7 @@ def default(self) -> Union[T, None, _Undefined]: @property def required(self) -> bool: - """Is this field required for serialization? + """Indicates if this field is required for serialization. :type: bool """ @@ -605,7 +592,7 @@ def get_expected_size(self, field_values: StrDict) -> int: .. versionchanged:: 0.9.0 This used to be a private method. ``_get_expected_size()`` is still present - for compatibility but it will eventually be removed. + for compatibility, but it will eventually be removed. """ if isinstance(self.size, int): return self.size @@ -618,13 +605,8 @@ def get_expected_size(self, field_values: StrDict) -> int: if isinstance(self.size, Field): name = self.size.name - elif isinstance(self.size, str): - name = self.size else: - raise TypeError( - "Unexpected type for %s.size: %s" - % (self.name, type(self.size).__name__) - ) + name = self.size if name in field_values: expected_size = field_values[name] @@ -635,12 +617,6 @@ def get_expected_size(self, field_values: StrDict) -> int: ) return expected_size - if isinstance(self._size, Field): - raise errors.FieldReferenceError( - f"Can't compute size for {self!r}; size references a field that hasn't" - f" been computed yet: {self._size!r}", - field=name, - ) raise errors.MissingRequiredValueError(field=name) def __get_expected_possibly_undefined_size(self, field_values: StrDict) -> int: @@ -656,14 +632,6 @@ def __get_expected_possibly_undefined_size(self, field_values: StrDict) -> int: elif self.default is not UNDEFINED: # Else: The value for this field isn't set, fall back to the default. value = self.default - # elif self.name is None: - # # The field is either unbound or embedded in another field, such as an Array - # # or Union. We have no way of getting the size from this. - # raise errors.UndefinedSizeError(field=self) - # else: - # # The field is bound but not present in the value dictionary. This happens - # # when loading. - # raise errors.MissingRequiredValueError(field=self) else: raise errors.UndefinedSizeError(field=self) @@ -717,13 +685,13 @@ def from_stream( # noqa: C901 if self.allow_null: try: null_repr = self._get_null_repr(loaded_fields) - except errors.UnserializableValueError as err: + except errors.UnserializableValueError: # Null can't be represented in this current state, so we can't check to # see if the *raw binary* form is null. This isn't an error UNLESS # null_value is `DEFAULT`. If null_value is DEFAULT and we can't # determine the size, then we're out of luck. if self.null_value is DEFAULT: - raise errors.CannotDetermineNullError(field=self) from err.__cause__ + raise errors.CannotDetermineNullError(field=self) from None else: potential_null_bytes = helpers.peek_bytes(stream, len(null_repr)) if potential_null_bytes == null_repr: @@ -797,7 +765,7 @@ def from_bytes( raise errors.ExtraneousDataError( "Expected to read %d bytes, read %d." % (stream.tell(), len(data)) ) - return loaded_data + return loaded_data # noqa: R504 @abc.abstractmethod def _do_load( @@ -1015,17 +983,17 @@ def _read_exact_size( @overload def __get__(self, instance: None, owner: Type["Struct"]) -> "Field[T]": - ... + ... # pragma: no cover @overload def __get__(self, instance: "Struct", owner: Type["Struct"]) -> Optional[T]: - ... + ... # pragma: no cover # This annotation is bogus and only here to make MyPy happy. See bug report here: # https://github.com/python/mypy/issues/9416 @overload def __get__(self, instance: "Field[Any]", owner: Type["Field[Any]"]) -> "Field[T]": - ... + ... # pragma: no cover def __get__(self, instance, owner): # type: ignore[no-untyped-def] if instance is None: diff --git a/binobj/fields/containers.py b/binobj/fields/containers.py index 54e55933..cfc25de1 100644 --- a/binobj/fields/containers.py +++ b/binobj/fields/containers.py @@ -130,8 +130,8 @@ def get_final_element_count(self, field_values: StrDict) -> Optional[int]: if isinstance(self.count, Field): name = self.count.name if name is None: - # This will only happen if someone creates a field outside of a Struct - # and passes it to this field as the count object. + # This will only happen if someone creates a field outside a Struct and + # passes it to this field as the count object. raise errors.ConfigurationError( "`count` field for %r has no assigned name." % self, field=self.count, @@ -139,6 +139,8 @@ def get_final_element_count(self, field_values: StrDict) -> Optional[int]: elif isinstance(self.count, str): name = self.count else: + # We check the type of `self.count` in the constructor so this should never + # happen. raise TypeError( "Unexpected type for `count`: %r" % type(self.count).__name__ ) @@ -208,7 +210,7 @@ def should_halt( """ if seq.count is not None: count = seq.get_final_element_count(loaded_fields) - if count is None: + if count is None: # pragma: no cover # Theoretically this should never happen, as get_final_element_count() # should only return None if seq.count is None. raise errors.UndefinedSizeError(field=seq) @@ -299,7 +301,7 @@ def _do_load( :return: The deserialized data. :rtype: list """ - result = [] # type: List[Optional[T]] + result: List[Optional[T]] = [] while not self.halt_check(self, stream, result, context, loaded_fields): component = self.component.from_stream(stream, context, loaded_fields) if component is NOT_PRESENT: @@ -457,13 +459,15 @@ def _do_dump( if isinstance(dumper, Field): dumper.to_stream(stream, data, context, all_fields) elif issubclass(dumper, Struct): - # Else: Dumper is not a Field instance, assume this is a Struct class. - # TODO (dargueta): Avoid creating a full struct instance if possible. + if not isinstance(data, collections.abc.Mapping): + raise TypeError( + f"Cannot dump a non-Mapping-like object as a {dumper!r}: {data!r}", + ) dumper(**data).to_stream(stream, context) else: raise TypeError( - f"Dump decider returned a {type(dumper)!r}, expected a Field instance" - " or subclass of Struct." + "Dump decider returned a %r, expected a Field instance or subclass of" + " Struct." % type(dumper) ) def _do_load(self, stream: BinaryIO, context: Any, loaded_fields: StrDict) -> Any: diff --git a/binobj/fields/stringlike.py b/binobj/fields/stringlike.py index ab51bc5b..98cf3061 100644 --- a/binobj/fields/stringlike.py +++ b/binobj/fields/stringlike.py @@ -223,7 +223,7 @@ def __init__( size = 36 elif stored_as is UUIDFormat.HEX_STRING: size = 32 - else: + else: # pragma: no cover raise NotImplementedError( f"BUG: The UUID4 storage format {stored_as!r} isn't implemented. Please" " file a bug report." diff --git a/binobj/pep526.py b/binobj/pep526.py index d1cf12d7..5d78e5fd 100644 --- a/binobj/pep526.py +++ b/binobj/pep526.py @@ -44,18 +44,18 @@ class MyStruct(binobj.Struct): from typing import TypeVar from typing import Union -import binobj from binobj import errors from binobj import fields +from binobj.structures import Struct __all__ = ["dataclass"] -TStruct = TypeVar("TStruct", bound=binobj.Struct) +TStruct = TypeVar("TStruct", bound=Struct) -try: +try: # pragma: no cover ( MutableStrDict: # Using ChainMap on Python 3.7 and 3.8 crashes for some reason when we try to # iterate over it. It appears to be the way that keys are cached. - # return collections.ChainMap(dct, self) + # Normally it'd just be: `return collections.ChainMap(dct, self)` dct.update({name: getattr(self, name) for name in self if name not in dct}) return dct @@ -461,7 +461,7 @@ def from_bytes( % (stream.tell() + 1, len(data)), offset=stream.tell(), ) - return loaded_data + return loaded_data # noqa: R504 @classmethod def partial_load( diff --git a/binobj/varints.py b/binobj/varints.py index 1ecf7cb1..3c50a09c 100644 --- a/binobj/varints.py +++ b/binobj/varints.py @@ -101,7 +101,10 @@ def decode_integer_compact(stream: BinaryIO) -> int: # Sign hasn't been determined yet so this must be the first byte of the # number. value = int8 & 0x3F - sign = -1 if int8 & 0x40 else 1 + if int8 & 0x40: + sign = -1 + else: + sign = 1 else: value = (value << 7) | (int8 & 0x7F) @@ -171,7 +174,10 @@ def encode_integer_uleb128(value: int) -> bytes: output = bytearray() while value > 0: - continue_bit = 0x80 if value > 127 else 0 + if value > 127: + continue_bit = 0x80 + else: + continue_bit = 0 output.append(continue_bit | (value & 0x7F)) value >>= 7 diff --git a/docs/source/CHANGELOG.rst b/docs/source/CHANGELOG.rst index 5d160603..35aff811 100644 --- a/docs/source/CHANGELOG.rst +++ b/docs/source/CHANGELOG.rst @@ -1,13 +1,38 @@ Changelog ========= +0.11.4 +------ + +Bugfixes +~~~~~~~~ + +Add all bugfixes from 0.11.2. (They were accidentally removed in 0.11.3 due to a +borked merge.) + +New Features +~~~~~~~~~~~~ + +* Added support for Python 3.12. +* The ``binobj`` package now exports the ``binobj.pep526.dataclass`` decorator + directly. + +Other Changes +~~~~~~~~~~~~~ + +* Stricter linting, remove dead code and make tests prettier. +* Upgrade test dependencies. +* Add ``typing-extensions`` and ``importlib-metadata`` as explicit dependencies. + We were relying on other libraries to install them before. + 0.11.3 ------ Bugfixes ~~~~~~~~ -``types.NoneType`` existed then was added back in 3.10. +``types.NoneType`` was removed in some versions of Python before being added back +in 3.10. We now handle that case in the annotation detection. 0.11.2 ------ diff --git a/poetry.lock b/poetry.lock index 6d8e7c70..6ed27912 100644 --- a/poetry.lock +++ b/poetry.lock @@ -11,15 +11,29 @@ files = [ {file = "astor-0.8.1.tar.gz", hash = "sha256:6a6effda93f4e1ce9f618779b2dd1d9d84f1e32812c23a29b3fff6fd7f63fa5e"}, ] +[[package]] +name = "astpretty" +version = "2.1.0" +description = "Pretty print the output of python stdlib `ast.parse`." +optional = false +python-versions = ">=3.6.1" +files = [ + {file = "astpretty-2.1.0-py2.py3-none-any.whl", hash = "sha256:f81f14b5636f7af81fadb1e3c09ca7702ce4615500d9cc6d6829befb2dec2e3c"}, + {file = "astpretty-2.1.0.tar.gz", hash = "sha256:8a801fcda604ec741f010bb36d7cbadc3ec8a182ea6fb83e20ab663463e75ff6"}, +] + +[package.extras] +typed = ["typed-ast"] + [[package]] name = "attrs" -version = "23.1.0" +version = "23.2.0" description = "Classes Without Boilerplate" optional = false python-versions = ">=3.7" files = [ - {file = "attrs-23.1.0-py3-none-any.whl", hash = "sha256:1f28b4522cdc2fb4256ac1a020c78acf9cba2c6b461ccd2c126f3aa8e8335d04"}, - {file = "attrs-23.1.0.tar.gz", hash = "sha256:6279836d581513a26f1bf235f9acd333bc9115683f14f7e8fae46c98fc50e015"}, + {file = "attrs-23.2.0-py3-none-any.whl", hash = "sha256:99b87a485a5820b23b879f04c2305b44b951b502fd64be915879d77a7e8fc6f1"}, + {file = "attrs-23.2.0.tar.gz", hash = "sha256:935dc3b529c262f6cf76e50877d35a4bd3c1de194fd41f47a2b7ae8f19971f30"}, ] [package.dependencies] @@ -27,10 +41,34 @@ importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} [package.extras] cov = ["attrs[tests]", "coverage[toml] (>=5.3)"] -dev = ["attrs[docs,tests]", "pre-commit"] +dev = ["attrs[tests]", "pre-commit"] docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope-interface"] tests = ["attrs[tests-no-zope]", "zope-interface"] -tests-no-zope = ["cloudpickle", "hypothesis", "mypy (>=1.1.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +tests-mypy = ["mypy (>=1.6)", "pytest-mypy-plugins"] +tests-no-zope = ["attrs[tests-mypy]", "cloudpickle", "hypothesis", "pympler", "pytest (>=4.3.0)", "pytest-xdist[psutil]"] + +[[package]] +name = "bandit" +version = "1.7.5" +description = "Security oriented static analyser for python code." +optional = false +python-versions = ">=3.7" +files = [ + {file = "bandit-1.7.5-py3-none-any.whl", hash = "sha256:75665181dc1e0096369112541a056c59d1c5f66f9bb74a8d686c3c362b83f549"}, + {file = "bandit-1.7.5.tar.gz", hash = "sha256:bdfc739baa03b880c2d15d0431b31c658ffc348e907fe197e54e0389dd59e11e"}, +] + +[package.dependencies] +colorama = {version = ">=0.3.9", markers = "platform_system == \"Windows\""} +GitPython = ">=1.0.1" +PyYAML = ">=5.3.1" +rich = "*" +stevedore = ">=1.20.0" + +[package.extras] +test = ["beautifulsoup4 (>=4.8.0)", "coverage (>=4.5.4)", "fixtures (>=3.0.0)", "flake8 (>=4.0.0)", "pylint (==1.9.4)", "stestr (>=2.5.0)", "testscenarios (>=0.5.0)", "testtools (>=2.3.0)", "tomli (>=1.1.0)"] +toml = ["tomli (>=1.1.0)"] +yaml = ["PyYAML"] [[package]] name = "black" @@ -202,13 +240,13 @@ pyparsing = ">=2.4.7" [[package]] name = "domdf-python-tools" -version = "3.7.0" +version = "3.8.0.post2" description = "Helpful functions for Python 🐍 🛠️" optional = false python-versions = ">=3.6" files = [ - {file = "domdf_python_tools-3.7.0-py3-none-any.whl", hash = "sha256:7b4d1c3bdb7402b872d43953824bf921ae2e52f893adbe5c0052a21a6efa2fe4"}, - {file = "domdf_python_tools-3.7.0.tar.gz", hash = "sha256:df1af9a91649af0fb2a4e7b3a4b0a0936e4f78389dd7280dd6fd2f53a339ca71"}, + {file = "domdf_python_tools-3.8.0.post2-py3-none-any.whl", hash = "sha256:ad2c763c8d00850a7fa92ad95e9891a1918281ea25322c4dbb1734fd32f905dd"}, + {file = "domdf_python_tools-3.8.0.post2.tar.gz", hash = "sha256:a1fd255ea29f767b08de462d2da39d360262304389227d980bc307ee8aa3366a"}, ] [package.dependencies] @@ -220,15 +258,26 @@ typing-extensions = ">=3.7.4.1" all = ["pytz (>=2019.1)"] dates = ["pytz (>=2019.1)"] +[[package]] +name = "eradicate" +version = "2.3.0" +description = "Removes commented-out code." +optional = false +python-versions = "*" +files = [ + {file = "eradicate-2.3.0-py3-none-any.whl", hash = "sha256:2b29b3dd27171f209e4ddd8204b70c02f0682ae95eecb353f10e8d72b149c63e"}, + {file = "eradicate-2.3.0.tar.gz", hash = "sha256:06df115be3b87d0fc1c483db22a2ebb12bcf40585722810d809cc770f5031c37"}, +] + [[package]] name = "exceptiongroup" -version = "1.1.3" +version = "1.2.0" description = "Backport of PEP 654 (exception groups)" optional = false python-versions = ">=3.7" files = [ - {file = "exceptiongroup-1.1.3-py3-none-any.whl", hash = "sha256:343280667a4585d195ca1cf9cef84a4e178c4b6cf2274caef9859782b567d5e3"}, - {file = "exceptiongroup-1.1.3.tar.gz", hash = "sha256:097acd85d473d75af5bb98e41b61ff7fe35efe6675e4f9370ec6ec5126d160e9"}, + {file = "exceptiongroup-1.2.0-py3-none-any.whl", hash = "sha256:4bfd3996ac73b41e9b9628b04e079f193850720ea5945fc96a08633c66912f14"}, + {file = "exceptiongroup-1.2.0.tar.gz", hash = "sha256:91f5c769735f051a4290d52edd0858999b57e5876e9f85937691bd4c9fa3ed68"}, ] [package.extras] @@ -251,6 +300,66 @@ mccabe = ">=0.7.0,<0.8.0" pycodestyle = ">=2.9.0,<2.10.0" pyflakes = ">=2.5.0,<2.6.0" +[[package]] +name = "flake8-annotations-complexity" +version = "0.0.8" +description = "A flake8 extension that checks for type annotations complexity" +optional = false +python-versions = ">=3.7" +files = [ + {file = "flake8_annotations_complexity-0.0.8-py3-none-any.whl", hash = "sha256:44e299dac0ab64f3af4b478fac7feb635c113a606036de8b39dadab3531da7ed"}, + {file = "flake8_annotations_complexity-0.0.8.tar.gz", hash = "sha256:8914e76e31672c4063c928b0a250db6b935a16d1799de10457958a71d11bb665"}, +] + +[package.dependencies] +flake8 = "*" + +[[package]] +name = "flake8-awesome" +version = "1.3.0" +description = "Flake8 awesome plugins pack" +optional = false +python-versions = ">=3.6,<4.0" +files = [ + {file = "flake8-awesome-1.3.0.tar.gz", hash = "sha256:bc6b807abcd25474259a184311e9c394f0eb265b0ea7aa93b4c802a835da40ac"}, + {file = "flake8_awesome-1.3.0-py3-none-any.whl", hash = "sha256:f2c6246432e8d38d6e25dd53b1622e6ab643913cf313a6a82e27d61f7cd8097f"}, +] + +[package.dependencies] +flake8 = "*" +flake8-annotations-complexity = "*" +flake8-bandit = "*" +flake8-breakpoint = "*" +flake8-bugbear = "*" +flake8-builtins = "*" +flake8-comprehensions = "*" +flake8-eradicate = "*" +flake8-expression-complexity = "*" +flake8-if-expr = "*" +flake8-isort = "*" +flake8-logging-format = "*" +flake8-print = "*" +flake8-pytest = "*" +flake8-pytest-style = "*" +flake8-requirements = "*" +flake8-return = "*" +pep8-naming = "*" + +[[package]] +name = "flake8-bandit" +version = "4.1.1" +description = "Automated security testing with bandit and flake8." +optional = false +python-versions = ">=3.6" +files = [ + {file = "flake8_bandit-4.1.1-py3-none-any.whl", hash = "sha256:4c8a53eb48f23d4ef1e59293657181a3c989d0077c9952717e98a0eace43e06d"}, + {file = "flake8_bandit-4.1.1.tar.gz", hash = "sha256:068e09287189cbfd7f986e92605adea2067630b75380c6b5733dab7d87f9a84e"}, +] + +[package.dependencies] +bandit = ">=1.7.3" +flake8 = ">=5.0.0" + [[package]] name = "flake8-black" version = "0.3.6" @@ -280,6 +389,20 @@ files = [ {file = "flake8-blind-except-0.2.1.tar.gz", hash = "sha256:f25a575a9dcb3eeb3c760bf9c22db60b8b5a23120224ed1faa9a43f75dd7dd16"}, ] +[[package]] +name = "flake8-breakpoint" +version = "1.1.0" +description = "Flake8 plugin that check forgotten breakpoints" +optional = false +python-versions = ">=3.6,<4.0" +files = [ + {file = "flake8-breakpoint-1.1.0.tar.gz", hash = "sha256:5bc70d478f0437a3655d094e1d2fca81ddacabaa84d99db45ad3630bf2004064"}, + {file = "flake8_breakpoint-1.1.0-py3-none-any.whl", hash = "sha256:27e0cb132647f9ef348b4a3c3126e7350bedbb22e8e221cd11712a223855ea0b"}, +] + +[package.dependencies] +flake8-plugin-utils = ">=1.0,<2.0" + [[package]] name = "flake8-bugbear" version = "23.3.12" @@ -377,6 +500,52 @@ files = [ flake8 = ">=3" pydocstyle = ">=2.1" +[[package]] +name = "flake8-eradicate" +version = "1.4.0" +description = "Flake8 plugin to find commented out code" +optional = false +python-versions = ">=3.7,<4.0" +files = [ + {file = "flake8-eradicate-1.4.0.tar.gz", hash = "sha256:3088cfd6717d1c9c6c3ac45ef2e5f5b6c7267f7504d5a74b781500e95cb9c7e1"}, + {file = "flake8_eradicate-1.4.0-py3-none-any.whl", hash = "sha256:e3bbd0871be358e908053c1ab728903c114f062ba596b4d40c852fd18f473d56"}, +] + +[package.dependencies] +attrs = "*" +eradicate = ">=2.0,<3.0" +flake8 = ">=3.5,<6" +importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} + +[[package]] +name = "flake8-expression-complexity" +version = "0.0.11" +description = "A flake8 extension that checks expressions complexity" +optional = false +python-versions = ">=3.7" +files = [ + {file = "flake8_expression_complexity-0.0.11-py3-none-any.whl", hash = "sha256:b56bac37f7dd5d3d102a7111c89f6579c2cbd897b868147794c9ed12aadc627c"}, + {file = "flake8_expression_complexity-0.0.11.tar.gz", hash = "sha256:4dd8909fecbc20f53814cdcef9d0b04f61532764278d9b6e8026686812e96631"}, +] + +[package.dependencies] +astpretty = "*" +flake8 = "*" + +[[package]] +name = "flake8-if-expr" +version = "1.0.4" +description = "The plugin checks `if expressions` (ternary operator)" +optional = false +python-versions = ">=3.6,<4.0" +files = [ + {file = "flake8-if-expr-1.0.4.tar.gz", hash = "sha256:e0050b59b46114b6e20628d61175a7a608d3eb55fb5b90d87db0f9352a91a491"}, + {file = "flake8_if_expr-1.0.4-py3-none-any.whl", hash = "sha256:3f2d45cc1e48b4cdf22377ca1624b2a59b6f1c7825ba1cdee99ff1e647e7ae3f"}, +] + +[package.dependencies] +flake8-plugin-utils = ">=1.0,<2.0" + [[package]] name = "flake8-implicit-str-concat" version = "0.4.0" @@ -437,15 +606,101 @@ files = [ [package.dependencies] flake8 = ">=3.0.0" +[[package]] +name = "flake8-plugin-utils" +version = "1.3.3" +description = "The package provides base classes and utils for flake8 plugin writing" +optional = false +python-versions = ">=3.6,<4.0" +files = [ + {file = "flake8-plugin-utils-1.3.3.tar.gz", hash = "sha256:39f6f338d038b301c6fd344b06f2e81e382b68fa03c0560dff0d9b1791a11a2c"}, + {file = "flake8_plugin_utils-1.3.3-py3-none-any.whl", hash = "sha256:e4848c57d9d50f19100c2d75fa794b72df068666a9041b4b0409be923356a3ed"}, +] + +[[package]] +name = "flake8-print" +version = "5.0.0" +description = "print statement checker plugin for flake8" +optional = false +python-versions = ">=3.7" +files = [ + {file = "flake8-print-5.0.0.tar.gz", hash = "sha256:76915a2a389cc1c0879636c219eb909c38501d3a43cc8dae542081c9ba48bdf9"}, + {file = "flake8_print-5.0.0-py3-none-any.whl", hash = "sha256:84a1a6ea10d7056b804221ac5e62b1cee1aefc897ce16f2e5c42d3046068f5d8"}, +] + +[package.dependencies] +flake8 = ">=3.0" +pycodestyle = "*" + +[[package]] +name = "flake8-pytest" +version = "1.4" +description = "pytest assert checker plugin for flake8" +optional = false +python-versions = "*" +files = [ + {file = "flake8-pytest-1.4.tar.gz", hash = "sha256:19f543b2d1cc89d61b76f19d0a9e58e9a110a035175f701b3425c363a7732c56"}, + {file = "flake8_pytest-1.4-py2.py3-none-any.whl", hash = "sha256:97328f258ffad9fe18babb3b0714a16b121505ad3ac87d4e33020874555d0784"}, +] + +[package.dependencies] +flake8 = "*" + +[[package]] +name = "flake8-pytest-style" +version = "1.6.0" +description = "A flake8 plugin checking common style issues or inconsistencies with pytest-based tests." +optional = false +python-versions = ">=3.6.2,<4.0.0" +files = [ + {file = "flake8-pytest-style-1.6.0.tar.gz", hash = "sha256:c1175713e9e11b78cd1a035ed0bca0d1e41d09c4af329a952750b61d4194ddac"}, + {file = "flake8_pytest_style-1.6.0-py3-none-any.whl", hash = "sha256:5fedb371a950e9fe0e0e6bfc854be7d99151271208f34cd2cc517681ece27780"}, +] + +[package.dependencies] +flake8-plugin-utils = ">=1.3.2,<2.0.0" + +[[package]] +name = "flake8-requirements" +version = "2.1.0" +description = "Package requirements checker, plugin for flake8" +optional = false +python-versions = "*" +files = [ + {file = "flake8_requirements-2.1.0-py3-none-any.whl", hash = "sha256:a437e55c936542943bd532130772575f95690df1f57ad85191cec007d78a2f96"}, +] + +[package.dependencies] +flake8 = ">=4.0.0" +setuptools = ">=10.0.0" +tomli = {version = ">=1.2.1", markers = "python_version < \"3.11\""} + +[package.extras] +pyproject = ["Flake8-pyproject"] + +[[package]] +name = "flake8-return" +version = "1.2.0" +description = "Flake8 plugin that checks return values" +optional = false +python-versions = ">=3.6,<4.0" +files = [ + {file = "flake8-return-1.2.0.tar.gz", hash = "sha256:68dfa56582cd704febd02ad86dcf5df67e38e0836d62f1ceae7930d76d3dd955"}, + {file = "flake8_return-1.2.0-py3-none-any.whl", hash = "sha256:1f07af12954ed03ebe2c2aac2418f78b55374e9929d4956109664588f31582a1"}, +] + +[package.dependencies] +flake8-plugin-utils = ">=1.0,<2.0" + [[package]] name = "flake8-simplify" -version = "0.20.0" +version = "0.21.0" description = "flake8 plugin which checks for code that can be simplified" optional = false python-versions = ">=3.6.1" files = [ - {file = "flake8_simplify-0.20.0-py3-none-any.whl", hash = "sha256:599a47824726c93fadcf0274e569daed45052e38cd906360d9080eaa3bd76d61"}, - {file = "flake8_simplify-0.20.0.tar.gz", hash = "sha256:7b8796bbea8aed45f56621c389d0556cc86f0afa5d992581139451240a8fbeca"}, + {file = "flake8_simplify-0.21.0-py3-none-any.whl", hash = "sha256:439391e762a9370b371208add0b5c5c40c3d25a98e1f5421d263215d08194183"}, + {file = "flake8_simplify-0.21.0.tar.gz", hash = "sha256:c95ff1dcc1de5949af47e0087cbf1164445881131b15bcd7a71252670f492f4d"}, ] [package.dependencies] @@ -453,6 +708,38 @@ astor = ">=0.1" flake8 = ">=3.7" importlib-metadata = {version = ">=0.9", markers = "python_version < \"3.8\""} +[[package]] +name = "gitdb" +version = "4.0.11" +description = "Git Object Database" +optional = false +python-versions = ">=3.7" +files = [ + {file = "gitdb-4.0.11-py3-none-any.whl", hash = "sha256:81a3407ddd2ee8df444cbacea00e2d038e40150acfa3001696fe0dcf1d3adfa4"}, + {file = "gitdb-4.0.11.tar.gz", hash = "sha256:bf5421126136d6d0af55bc1e7c1af1c397a34f5b7bd79e776cd3e89785c2b04b"}, +] + +[package.dependencies] +smmap = ">=3.0.1,<6" + +[[package]] +name = "gitpython" +version = "3.1.42" +description = "GitPython is a Python library used to interact with Git repositories" +optional = false +python-versions = ">=3.7" +files = [ + {file = "GitPython-3.1.42-py3-none-any.whl", hash = "sha256:1bf9cd7c9e7255f77778ea54359e54ac22a72a5b51288c457c881057b7bb9ecd"}, + {file = "GitPython-3.1.42.tar.gz", hash = "sha256:2d99869e0fef71a73cbd242528105af1d6c1b108c60dfabd994bf292f76c3ceb"}, +] + +[package.dependencies] +gitdb = ">=4.0.1,<5" +typing-extensions = {version = ">=3.7.4.3", markers = "python_version < \"3.8\""} + +[package.extras] +test = ["black", "coverage[toml]", "ddt (>=1.1.1,!=1.4.3)", "mock", "mypy", "pre-commit", "pytest (>=7.3.1)", "pytest-cov", "pytest-instafail", "pytest-mock", "pytest-sugar"] + [[package]] name = "importlib-metadata" version = "4.2.0" @@ -500,6 +787,31 @@ pipfile-deprecated-finder = ["pip-shims (>=0.5.2)", "pipreqs", "requirementslib" plugins = ["setuptools"] requirements-deprecated-finder = ["pip-api", "pipreqs"] +[[package]] +name = "markdown-it-py" +version = "2.2.0" +description = "Python port of markdown-it. Markdown parsing, done right!" +optional = false +python-versions = ">=3.7" +files = [ + {file = "markdown-it-py-2.2.0.tar.gz", hash = "sha256:7c9a5e412688bc771c67432cbfebcdd686c93ce6484913dccf06cb5a0bea35a1"}, + {file = "markdown_it_py-2.2.0-py3-none-any.whl", hash = "sha256:5a35f8d1870171d9acc47b99612dc146129b631baf04970128b568f190d0cc30"}, +] + +[package.dependencies] +mdurl = ">=0.1,<1.0" +typing_extensions = {version = ">=3.7.4", markers = "python_version < \"3.8\""} + +[package.extras] +benchmarking = ["psutil", "pytest", "pytest-benchmark"] +code-style = ["pre-commit (>=3.0,<4.0)"] +compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "mistletoe (>=1.0,<2.0)", "mistune (>=2.0,<3.0)", "panflute (>=2.3,<3.0)"] +linkify = ["linkify-it-py (>=1,<3)"] +plugins = ["mdit-py-plugins"] +profiling = ["gprof2dot"] +rtd = ["attrs", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"] +testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] + [[package]] name = "mccabe" version = "0.7.0" @@ -511,6 +823,17 @@ files = [ {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, ] +[[package]] +name = "mdurl" +version = "0.1.2" +description = "Markdown URL utilities" +optional = false +python-versions = ">=3.7" +files = [ + {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, + {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, +] + [[package]] name = "more-itertools" version = "9.1.0" @@ -597,13 +920,13 @@ icu = ["PyICU (>=1.0.0)"] [[package]] name = "packaging" -version = "23.2" +version = "24.0" description = "Core utilities for Python packages" optional = false python-versions = ">=3.7" files = [ - {file = "packaging-23.2-py3-none-any.whl", hash = "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7"}, - {file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"}, + {file = "packaging-24.0-py3-none-any.whl", hash = "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5"}, + {file = "packaging-24.0.tar.gz", hash = "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9"}, ] [[package]] @@ -617,6 +940,31 @@ files = [ {file = "pathspec-0.11.2.tar.gz", hash = "sha256:e0d8d0ac2f12da61956eb2306b69f9469b42f4deb0f3cb6ed47b9cce9996ced3"}, ] +[[package]] +name = "pbr" +version = "6.0.0" +description = "Python Build Reasonableness" +optional = false +python-versions = ">=2.6" +files = [ + {file = "pbr-6.0.0-py2.py3-none-any.whl", hash = "sha256:4a7317d5e3b17a3dccb6a8cfe67dab65b20551404c52c8ed41279fa4f0cb4cda"}, + {file = "pbr-6.0.0.tar.gz", hash = "sha256:d1377122a5a00e2f940ee482999518efe16d745d423a670c27773dfbc3c9a7d9"}, +] + +[[package]] +name = "pep8-naming" +version = "0.13.3" +description = "Check PEP-8 naming conventions, plugin for flake8" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pep8-naming-0.13.3.tar.gz", hash = "sha256:1705f046dfcd851378aac3be1cd1551c7c1e5ff363bacad707d43007877fa971"}, + {file = "pep8_naming-0.13.3-py3-none-any.whl", hash = "sha256:1a86b8c71a03337c97181917e2b472f0f5e4ccb06844a0d6f0a33522549e7a80"}, +] + +[package.dependencies] +flake8 = ">=5.0.0" + [[package]] name = "platformdirs" version = "4.0.0" @@ -693,15 +1041,30 @@ files = [ {file = "pyflakes-2.5.0.tar.gz", hash = "sha256:491feb020dca48ccc562a8c0cbe8df07ee13078df59813b83959cbdada312ea3"}, ] +[[package]] +name = "pygments" +version = "2.17.2" +description = "Pygments is a syntax highlighting package written in Python." +optional = false +python-versions = ">=3.7" +files = [ + {file = "pygments-2.17.2-py3-none-any.whl", hash = "sha256:b27c2826c47d0f3219f29554824c30c5e8945175d888647acd804ddd04af846c"}, + {file = "pygments-2.17.2.tar.gz", hash = "sha256:da46cec9fd2de5be3a8a784f434e4c4ab670b4ff54d605c4c2717e9d49c4c367"}, +] + +[package.extras] +plugins = ["importlib-metadata"] +windows-terminal = ["colorama (>=0.4.6)"] + [[package]] name = "pyparsing" -version = "3.1.1" +version = "3.1.2" description = "pyparsing module - Classes and methods to define and execute parsing grammars" optional = false python-versions = ">=3.6.8" files = [ - {file = "pyparsing-3.1.1-py3-none-any.whl", hash = "sha256:32c7c0b711493c72ff18a981d24f28aaf9c1fb7ed5e9667c9e84e3db623bdbfb"}, - {file = "pyparsing-3.1.1.tar.gz", hash = "sha256:ede28a1a32462f5a9705e07aea48001a08f7cf81a021585011deba701581a0db"}, + {file = "pyparsing-3.1.2-py3-none-any.whl", hash = "sha256:f9db75911801ed778fe61bb643079ff86601aca99fcae6345aa67292038fb742"}, + {file = "pyparsing-3.1.2.tar.gz", hash = "sha256:a1bac0ce561155ecc3ed78ca94d3c9378656ad4c94c1270de543f621420f94ad"}, ] [package.extras] @@ -709,13 +1072,13 @@ diagrams = ["jinja2", "railroad-diagrams"] [[package]] name = "pytest" -version = "7.4.2" +version = "7.4.3" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.7" files = [ - {file = "pytest-7.4.2-py3-none-any.whl", hash = "sha256:1d881c6124e08ff0a1bb75ba3ec0bfd8b5354a01c194ddd5a0a870a48d99b002"}, - {file = "pytest-7.4.2.tar.gz", hash = "sha256:a766259cfab564a2ad52cb1aae1b881a75c3eb7e34ca3779697c23ed47c47069"}, + {file = "pytest-7.4.3-py3-none-any.whl", hash = "sha256:0d009c083ea859a71b76adf7c1d502e4bc170b80a8ef002da5806527b9591fac"}, + {file = "pytest-7.4.3.tar.gz", hash = "sha256:d989d136982de4e3b29dabcc838ad581c64e8ed52c11fbe86ddebd9da0818cd5"}, ] [package.dependencies] @@ -763,6 +1126,111 @@ files = [ importlib-metadata = {version = ">=3.6.0", markers = "python_version < \"3.10\""} pytest = "*" +[[package]] +name = "pyyaml" +version = "6.0.1" +description = "YAML parser and emitter for Python" +optional = false +python-versions = ">=3.6" +files = [ + {file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"}, + {file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, + {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"}, + {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, + {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, + {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, + {file = "PyYAML-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, + {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"}, + {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, + {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, + {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, + {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, + {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, + {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, + {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, + {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"}, + {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd"}, + {file = "PyYAML-6.0.1-cp36-cp36m-win32.whl", hash = "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585"}, + {file = "PyYAML-6.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa"}, + {file = "PyYAML-6.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c"}, + {file = "PyYAML-6.0.1-cp37-cp37m-win32.whl", hash = "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba"}, + {file = "PyYAML-6.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867"}, + {file = "PyYAML-6.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, + {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"}, + {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, + {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, + {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, + {file = "PyYAML-6.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, + {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"}, + {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, + {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, + {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, +] + +[[package]] +name = "rich" +version = "13.7.1" +description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" +optional = false +python-versions = ">=3.7.0" +files = [ + {file = "rich-13.7.1-py3-none-any.whl", hash = "sha256:4edbae314f59eb482f54e9e30bf00d33350aaa94f4bfcd4e9e3110e64d0d7222"}, + {file = "rich-13.7.1.tar.gz", hash = "sha256:9be308cb1fe2f1f57d67ce99e95af38a1e2bc71ad9813b0e247cf7ffbcc3a432"}, +] + +[package.dependencies] +markdown-it-py = ">=2.2.0" +pygments = ">=2.13.0,<3.0.0" +typing-extensions = {version = ">=4.0.0,<5.0", markers = "python_version < \"3.9\""} + +[package.extras] +jupyter = ["ipywidgets (>=7.5.1,<9)"] + +[[package]] +name = "setuptools" +version = "68.0.0" +description = "Easily download, build, install, upgrade, and uninstall Python packages" +optional = false +python-versions = ">=3.7" +files = [ + {file = "setuptools-68.0.0-py3-none-any.whl", hash = "sha256:11e52c67415a381d10d6b462ced9cfb97066179f0e871399e006c4ab101fc85f"}, + {file = "setuptools-68.0.0.tar.gz", hash = "sha256:baf1fdb41c6da4cd2eae722e135500da913332ab3f2f5c7d33af9b492acb5235"}, +] + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (==0.8.3)", "sphinx-reredirects", "sphinxcontrib-towncrier"] +testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pip-run (>=8.8)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] +testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] + +[[package]] +name = "smmap" +version = "5.0.1" +description = "A pure Python implementation of a sliding window memory map manager" +optional = false +python-versions = ">=3.7" +files = [ + {file = "smmap-5.0.1-py3-none-any.whl", hash = "sha256:e6d8668fa5f93e706934a62d7b4db19c8d9eb8cf2adbb75ef1b675aa332b69da"}, + {file = "smmap-5.0.1.tar.gz", hash = "sha256:dceeb6c0028fdb6734471eb07c0cd2aae706ccaecab45965ee83f11c8d3b1f62"}, +] + [[package]] name = "snowballstemmer" version = "2.2.0" @@ -774,6 +1242,21 @@ files = [ {file = "snowballstemmer-2.2.0.tar.gz", hash = "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1"}, ] +[[package]] +name = "stevedore" +version = "3.5.2" +description = "Manage dynamic plugins for Python applications" +optional = false +python-versions = ">=3.6" +files = [ + {file = "stevedore-3.5.2-py3-none-any.whl", hash = "sha256:fa2630e3d0ad3e22d4914aff2501445815b9a4467a6edc49387c667a38faf5bf"}, + {file = "stevedore-3.5.2.tar.gz", hash = "sha256:cf99f41fc0d5a4f185ca4d3d42b03be9011b0a1ec1a4ea1a282be1b4b306dcc2"}, +] + +[package.dependencies] +importlib-metadata = {version = ">=1.7.0", markers = "python_version < \"3.8\""} +pbr = ">=2.0.0,<2.1.0 || >2.1.0" + [[package]] name = "tomli" version = "2.0.1" @@ -837,13 +1320,13 @@ files = [ [[package]] name = "types-setuptools" -version = "68.2.0.1" +version = "69.0.0.0" description = "Typing stubs for setuptools" optional = false python-versions = ">=3.7" files = [ - {file = "types-setuptools-68.2.0.1.tar.gz", hash = "sha256:8f31e8201e7969789e0eb23463b53ebe5f67d92417df4b648a6ea3c357ca4f51"}, - {file = "types_setuptools-68.2.0.1-py3-none-any.whl", hash = "sha256:e9c649559743e9f98c924bec91eae97f3ba208a70686182c3658fd7e81778d37"}, + {file = "types-setuptools-69.0.0.0.tar.gz", hash = "sha256:b0a06219f628c6527b2f8ce770a4f47550e00d3e8c3ad83e2dc31bc6e6eda95d"}, + {file = "types_setuptools-69.0.0.0-py3-none-any.whl", hash = "sha256:8c86195bae2ad81e6dea900a570fe9d64a59dbce2b11cc63c046b03246ea77bf"}, ] [[package]] @@ -890,4 +1373,4 @@ testing = ["big-O", "flake8 (<5)", "jaraco.functools", "jaraco.itertools", "more [metadata] lock-version = "2.0" python-versions = "^3.7" -content-hash = "718422d61ceccf140928e8b0c5fc56e18936449e54687ac3c095b24714b2cbe4" +content-hash = "ba0a9b084f377149209300f41a5d34bc6397d99508289695425570afc0968c10" diff --git a/pyproject.toml b/pyproject.toml index 6d80c1da..75236e2f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "binobj" -version = "0.11.3" +version = "0.11.4" description = "A Python library for reading and writing structured binary data." authors = ["Diego Argueta <620513-dargueta@users.noreply.github.com>"] readme = "README.rst" @@ -15,6 +15,7 @@ classifiers = [ "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Typing :: Typed", @@ -29,7 +30,9 @@ exclude = [ [tool.poetry.dependencies] python = "^3.7" more-itertools = ">=4.0" +typing-extensions = ">=4" typing-inspect = {version = ">=0.4.0", python = "<3.8"} +importlib_metadata = {version = "*", python = "<3.8"} [tool.poetry.group.dev.dependencies] # For reproducibility please pin all direct dependencies as tightly as possible, @@ -38,6 +41,7 @@ black = "23.3.0" coverage = "7.2.7" coverage-pyver-pragma = "0.3.2" flake8 = "5.0.4" +flake8-awesome = "1.3.0" flake8-black = "0.3.6" flake8-blind-except = "0.2.1" flake8-bugbear = "23.3.12" @@ -50,12 +54,12 @@ flake8-implicit-str-concat = "0.4.0" flake8-isort = "6.0.0" flake8-logging-format = "0.9.0" flake8-loopy = "1.1.0" -flake8-simplify = "0.20.0" +flake8-simplify = "0.21.0" # Plugin broke and gave a bunch of false alarms # flake8-unused-arguments = "0.0.13" isort = "5.11.5" mypy = {version = "1.4.1", markers = "platform_python_implementation == 'CPython'"} -pytest = "7.4.2" +pytest = "7.4.3" pytest-cov = "4.1.0" pytest-randomly = "3.12.0" diff --git a/tests/conftest.py b/tests/conftest.py deleted file mode 100644 index 229ab314..00000000 --- a/tests/conftest.py +++ /dev/null @@ -1,10 +0,0 @@ -import sys - - -#: List of files to ignore on a specific collection run. -collect_ignore = [] - - -# Ignore the PEP 526 tests if we're on Python < 3.6 since it's invalid syntax -if sys.version_info < (3, 6): - collect_ignore.append("pep526_test.py") diff --git a/tests/decorators_test.py b/tests/decorators_test.py index d4e90bb5..e7915e18 100644 --- a/tests/decorators_test.py +++ b/tests/decorators_test.py @@ -49,7 +49,7 @@ class Class(binobj.Struct): blah = fields.UInt16(const=1234) @blah.computes - def _blah(self, all_fields): + def _blah(self, _all_fields): return 5678 diff --git a/tests/errors_test.py b/tests/errors_test.py index 7a880c0b..8035c7a2 100644 --- a/tests/errors_test.py +++ b/tests/errors_test.py @@ -11,13 +11,13 @@ def test_configuration_error__no_args_crashes(): @pytest.mark.parametrize( - "field, struct, obj, message", - ( + ("field", "struct", "obj", "message"), + [ ("foo", None, None, "The field 'foo' was misconfigured."), (None, "bar", None, "The struct 'bar' was misconfigured."), (None, None, "baz", "The object 'baz' was misconfigured."), ("foo", "bar", None, "Field 'foo' in struct 'bar' was misconfigured."), - ), + ], ) def test_configurationerror__default_messages(field, struct, obj, message): err = errors.ConfigurationError(field=field, struct=struct, obj=obj) @@ -25,8 +25,8 @@ def test_configurationerror__default_messages(field, struct, obj, message): @pytest.mark.parametrize( - "fields, message", - ( + ("fields", "message"), + [ ("blah", "1 unrecognized field(s) given to NoneType for serialization: 'blah'"), ( ("asdf", "ghjkl"), @@ -36,7 +36,7 @@ def test_configurationerror__default_messages(field, struct, obj, message): ("abc", "def", "abc"), "2 unrecognized field(s) given to NoneType for serialization: 'abc', 'def'", ), - ), + ], ) def test_unexpectedvalueerror__field_list(fields, message): err = errors.UnexpectedValueError(name=fields, struct=None) diff --git a/tests/fields/base_test.py b/tests/fields/base_test.py index fd24884d..a6c475d0 100644 --- a/tests/fields/base_test.py +++ b/tests/fields/base_test.py @@ -120,7 +120,7 @@ def test_dump__null_with_default_and_varlen(): field.to_bytes(None) -@pytest.mark.parametrize("size_field", (fields.Int32(name="size"), "size")) +@pytest.mark.parametrize("size_field", [fields.Int32(name="size"), "size"]) def test_dump__null_with_default_and_field_ref(size_field): """Successfully dump the expected number of nulls if the field is of variable length but has a defined size.""" @@ -359,7 +359,7 @@ def test_present__dump__not_present_not_given(): assert struct.to_bytes() == b"\x01\x00\x37\x13" -@pytest.mark.parametrize("thing", (fields.UNDEFINED, fields.NOT_PRESENT)) +@pytest.mark.parametrize("thing", [fields.UNDEFINED, fields.NOT_PRESENT]) def test_present__dump_not_present_given_something(thing): """If a field whose presence is controlled by something else is ``UNDEFINED`` or ``NOT_PRESENT`` it shouldn't be dumped. @@ -404,6 +404,13 @@ def test_callable_default_warns(): fields.Int32(name="whatever", default=lambda: 123) +@pytest.mark.xfail() +def test_callable_default_crashes(): + """Passing a callable for `default` triggers a TypeError.""" + with pytest.raises(TypeError): + fields.Int32(name="whatever", default=lambda: 123) + + def test_default_and_factory_triggers_error(): with pytest.raises( errors.ConfigurationError, @@ -412,7 +419,8 @@ def test_default_and_factory_triggers_error(): fields.Int8(name="asdf", default=123, factory=lambda: 123) -def test_explicit_name_mismatch_triggers_error(): +@pytest.mark.skipif(sys.version_info >= (3, 12), reason="Test requires 3.11 or lower") +def test_explicit_name_mismatch_triggers_error__py311_and_under(): """Passing a name to a field that already had its name assigned by ``__set_name__`` *and* it doesn't match triggers an error. """ @@ -428,6 +436,21 @@ class Broken(binobj.Struct): assert isinstance(cause, errors.ConfigurationError) +@pytest.mark.skipif(sys.version_info < (3, 12), reason="Test requires 3.12 or higher") +def test_explicit_name_mismatch_triggers_error(): + """Passing a name to a field that already had its name assigned by ``__set_name__`` + *and* it doesn't match triggers an error. + """ + with pytest.raises( + errors.ConfigurationError, + match=r"A name has already been set for this field \('qwerty'\) but an explicit" + r" name was also passed to the constructor \('asdf'\)\.", + ): + + class Broken(binobj.Struct): + asdf = fields.Bytes(name="qwerty", size=10) + + def test_explicit_name_matches_no_error(): """Passing a name to a field that already had its name assigned by ``__set_name__`` but the names match will not trigger an error. @@ -441,9 +464,36 @@ class ThisIsFine(binobj.Struct): def test_simulated_infinite_recursion(): class BorkedField(fields.Field[int]): - def _size_for_value(self, value): + def _size_for_value(self, _value): raise RecursionError borked = BorkedField(name="asdf", default=1) with pytest.raises(errors.BuggyFieldImplementationError): borked.get_expected_size({}) + + +def test_noninteger_size_field_crashes(): + """If a Field is given for ``size``, it must be an integer type.""" + field = fields.String(name="foo", size=fields.String(name="bar", size=4)) + + with pytest.raises( + TypeError, + match="Field 'foo' relies on field 'bar' to give its size, but 'bar' has a" + " non-integer value: '8'", + ): + field.from_bytes(b"sometext", loaded_fields={"bar": "8"}) + + +def test_get_expected_size_null_repr(): + """If a Field is given for ``size``, it must be an integer type.""" + field = fields.String(name="foo", null_value=b"NULL") + assert field.get_expected_size({"foo": None}) == 4 + + +def test_unserializable_null(): + field = fields.StringZ(null_value=fields.DEFAULT) + with pytest.raises(errors.CannotDetermineNullError) as errinfo: + field.from_bytes(b"string\x00") + + assert errinfo.value.field is field + assert errinfo.value.__cause__ is None diff --git a/tests/fields/containers_test.py b/tests/fields/containers_test.py index 6bc8994b..8e377ff6 100644 --- a/tests/fields/containers_test.py +++ b/tests/fields/containers_test.py @@ -40,7 +40,7 @@ def test_nested__dump_basic(): @pytest.mark.parametrize( "data", - ({"first": 0x0FAD, "second": "HllWrld"}, SubStruct(first=0x0FAD, second="HllWrld")), + [{"first": 0x0FAD, "second": "HllWrld"}, SubStruct(first=0x0FAD, second="HllWrld")], ) def test_nested__dump_basic_dict_or_instance(data): """Test dumping both a dict and an instance""" @@ -155,7 +155,7 @@ class BasicStructWithArraySizeFieldAsName(binobj.Struct): @pytest.mark.parametrize( - "cls", (BasicStructWithArraySizeField, BasicStructWithArraySizeFieldAsName) + "cls", [BasicStructWithArraySizeField, BasicStructWithArraySizeFieldAsName] ) def test_array__variable_length_size_in_struct(cls): stream = io.BytesIO(b"\x03\x01\x02\x7fABC") @@ -184,7 +184,7 @@ class _Crash(binobj.Struct): _Crash.from_bytes(b"\0\0ABC") -@pytest.mark.parametrize("count", (True, False, object())) +@pytest.mark.parametrize("count", [True, False, object()]) def test_array__bogus_count(count): with pytest.raises(TypeError): fields.Array(fields.UInt8(), count=count) @@ -196,7 +196,11 @@ def test_array__dump_basic(): @pytest.mark.parametrize( - "iterable", (["abc", "123456"], (s for s in ["abc", "123456"])) # Sized # Unsized + "iterable", + [ + pytest.param(["abc", "123456"], id="sized"), + pytest.param((s for s in ["abc", "123456"]), id="unsized"), + ], ) def test_array__sized_dump_ok(iterable): """Write a sized array with the expected number of values.""" @@ -266,6 +270,21 @@ def test_array__computed_size(): assert bytes(struct) == b"\x03\x06\x01\x01\x02\x03\x05\x08" +def test_array__unbound_count_field(): + """An Array can't be passed an unbound Field for its `count` argument.""" + array = fields.Array(fields.Int8(), count=fields.Int8()) + with pytest.raises(errors.ConfigurationError): + array.from_bytes(b"") + + +def test_array__count_wrong_type(): + """An Array can't be passed an unbound Field for its `count` argument.""" + array = fields.Array(fields.Int8()) + array.count = object() + with pytest.raises(TypeError, match="Unexpected type for `count`: 'object'"): + array.from_bytes(b"") + + class UnionItemA(binobj.Struct): _id = fields.UInt8(const=0xFF) value = fields.StringZ() @@ -296,8 +315,22 @@ class UnionContainer(binobj.Struct): ) -@pytest.mark.parametrize("item1", ({"other": 0xAA55}, UnionItemB(other=0xAA55))) -@pytest.mark.parametrize("item0", ({"value": "asdf"}, UnionItemA(value="asdf"))) +@pytest.mark.parametrize( + ("data_type", "item", "expected"), + [ + pytest.param(0, {"value": "asdf"}, b"\0\xffasdf\0"), + pytest.param(1, {"other": 0xAA55}, b"\x01\x7f\x55\xaa"), + pytest.param(0, UnionItemA(value="asdf"), b"\0\xffasdf\0"), + ], +) +def test_union__structs__dump_basic__dict(data_type, item, expected): + """Basic test of dumping the Union field type.""" + struct = UnionContainer(data_type=data_type, item=item) + assert struct.to_bytes() == expected + + +@pytest.mark.parametrize("item1", [{"other": 0xAA55}, UnionItemB(other=0xAA55)]) +@pytest.mark.parametrize("item0", [{"value": "asdf"}, UnionItemA(value="asdf")]) def test_union__structs__dump_basic(item0, item1): """Basic test of dumping the Union field type.""" struct = UnionContainer(data_type=0, item=item0) @@ -307,7 +340,7 @@ def test_union__structs__dump_basic(item0, item1): assert struct.to_bytes() == b"\x01\x7f\x55\xaa" -@pytest.mark.xfail +@pytest.mark.xfail() def test_union__structs__bad_data(): # Because we convert structs to dicts before serializing, serialization crashes early. # `item` should be UnionItemA, deliberately passing the wrong one @@ -370,3 +403,18 @@ def test_union__field_class_crashes(): fields.Union(fields.StringZ, load_decider=None, dump_decider=None) assert str(errinfo.value) == "You must pass an instance of a Field, not a class." + + +def test_union__dump_non_mapping_for_struct(): + """If the dump decider returns a Struct as the serializer,""" + field = fields.Union( + UnionContainer, + fields.StringZ(), + load_decider=None, + dump_decider=(lambda _s, classes, _ctx, _fields: classes[0]), + ) + + with pytest.raises( + TypeError, match="Cannot dump a non-Mapping-like object as a .+: 'foo'" + ): + field.to_bytes("foo") diff --git a/tests/fields/numeric_test.py b/tests/fields/numeric_test.py index ae55f4bf..f26d3d4d 100644 --- a/tests/fields/numeric_test.py +++ b/tests/fields/numeric_test.py @@ -57,7 +57,7 @@ def test_varint__underflow(): @pytest.mark.parametrize( - "value,expected", ((127, b"\x7f"), (0x1234567890, b"\x82\xa3\xa2\xd9\xf1\x10")) + ("value", "expected"), [(127, b"\x7f"), (0x1234567890, b"\x82\xa3\xa2\xd9\xf1\x10")] ) def test_varint__basic_dump(value, expected): """Test VLQ dump. @@ -69,7 +69,7 @@ def test_varint__basic_dump(value, expected): assert field.to_bytes(value) == expected -@pytest.mark.parametrize("data, expected", ((b"\x3f", 0x3F),)) +@pytest.mark.parametrize(("data", "expected"), [(b"\x3f", 0x3F)]) def test_varint__basic_load(data, expected): """Test VLQ load.""" field = numeric.VariableLengthInteger(vli_format=varints.VarIntEncoding.VLQ) @@ -87,7 +87,7 @@ def test_varint__max_bytes(): @pytest.mark.parametrize( - "null_value,serialized", ((-1, b"\x7f"), (b"\x7f\xff", b"\x7f\xff")) + ("null_value", "serialized"), [(-1, b"\x7f"), (b"\x7f\xff", b"\x7f\xff")] ) def test_varint__dump_null_value(null_value, serialized): field = numeric.VariableLengthInteger( @@ -97,7 +97,7 @@ def test_varint__dump_null_value(null_value, serialized): @pytest.mark.parametrize( - "null_value,serialized", ((-1, b"\x7f"), (b"\x7f\xff", b"\x7f\xff")) + ("null_value", "serialized"), [(-1, b"\x7f"), (b"\x7f\xff", b"\x7f\xff")] ) def test_varint__load_null_value(null_value, serialized): field = numeric.VariableLengthInteger( @@ -155,29 +155,29 @@ def test_float_bad_endian_crashes(): @pytest.mark.parametrize( - "field_object, fmt_string", - ( + ("field_object", "fmt_string"), + [ (numeric.Float32(endian="little"), "f"), (numeric.Float64(endian="big"), ">d"), - ), + ], ) -@pytest.mark.parametrize("value", (math.pi, math.inf, -math.inf, math.nan)) +@pytest.mark.parametrize("value", [math.pi, math.inf, -math.inf, math.nan]) def test_float__dumps(value, field_object, fmt_string): assert field_object.to_bytes(value) == struct.pack(fmt_string, value) @pytest.mark.parametrize( - "field_object, fmt_string", - ( + ("field_object", "fmt_string"), + [ (numeric.Float32(endian="little"), "f"), (numeric.Float64(endian="big"), ">d"), - ), + ], ) -@pytest.mark.parametrize("value", (math.pi, math.inf, -math.inf)) +@pytest.mark.parametrize("value", [math.pi, math.inf, -math.inf]) def test_float__loads(value, field_object, fmt_string): assert field_object.from_bytes(struct.pack(fmt_string, value)) == pytest.approx( value @@ -219,8 +219,8 @@ def test_timestamp__invalid_resolution(): @pytest.mark.parametrize( - "data,expected", - ( + ("data", "expected"), + [ ( b"\0\0\0\x80", datetime.datetime(1901, 12, 13, 20, 45, 52, tzinfo=datetime.timezone.utc), @@ -229,7 +229,7 @@ def test_timestamp__invalid_resolution(): b"\xff\xff\xff\x7f", datetime.datetime(2038, 1, 19, 3, 14, 7, tzinfo=datetime.timezone.utc), ), - ), + ], ) def test_timestamp__loads__naive(data, expected): field = numeric.Timestamp32(endian="little", tz_aware=True) @@ -306,7 +306,7 @@ def test_timestamp__aware_round_trip_truncated(): assert field.from_bytes(field.to_bytes(now)) == now_rounded -@pytest.mark.parametrize("tz", (None, datetime.timezone.utc)) +@pytest.mark.parametrize("tz", [None, datetime.timezone.utc]) def test_2038_problem_caught__dumping(tz): """32-bit signed timestamps crash if we try serializing a date past the Epochalypse. diff --git a/tests/fields/stringlike_test.py b/tests/fields/stringlike_test.py index 209eecc8..a69ba837 100644 --- a/tests/fields/stringlike_test.py +++ b/tests/fields/stringlike_test.py @@ -60,19 +60,19 @@ def test_string__dump_no_size(): field.to_bytes("asdf") -@pytest.mark.parametrize("null_value", ("NULL", b"NULL")) +@pytest.mark.parametrize("null_value", ["NULL", b"NULL"]) def test_string__dump_null_default(null_value): field = stringlike.String(null_value=null_value, default=None, size=4) assert field.to_bytes(None) == b"NULL" -@pytest.mark.parametrize("null_value", ("NULL", b"NULL")) +@pytest.mark.parametrize("null_value", ["NULL", b"NULL"]) def test_string__dump_null_factory(null_value): field = stringlike.String(null_value=null_value, factory=lambda: None, size=4) assert field.to_bytes(None) == b"NULL" -@pytest.mark.parametrize("size_field", (fields.UInt8(name="size"), "size")) +@pytest.mark.parametrize("size_field", [fields.UInt8(name="size"), "size"]) def test_string__dump_variable_size(size_field): """Dumping a field with variable size should work.""" field = stringlike.String(size=size_field) @@ -133,9 +133,9 @@ def test_stringz__dump_null_default(): @pytest.mark.parametrize( - "default_value_kwarg", ({"default": None}, {"factory": lambda: None}) + "default_value_kwarg", [{"default": None}, {"factory": lambda: None}] ) -@pytest.mark.parametrize("null_value", ("NULL", b"NULL\x00")) +@pytest.mark.parametrize("null_value", ["NULL", b"NULL\x00"]) def test_stringz__unsized_dump_null_default(null_value, default_value_kwarg): field = stringlike.StringZ(null_value=null_value, **default_value_kwarg) assert field.to_bytes(None) == b"NULL\x00" @@ -175,7 +175,7 @@ def test_string__load_null_default(): assert field.from_bytes(b"\x00" * 7) is None -@pytest.mark.parametrize("null_value", (b"N\x00U\x00L\x00L\x00", "NULL")) +@pytest.mark.parametrize("null_value", [b"N\x00U\x00L\x00L\x00", "NULL"]) def test_string__null_value(null_value): field = fields.String(size=8, null_value=null_value, encoding="utf-16-le") assert field.from_bytes(b"N\x00U\x00L\x00L\x00") is None @@ -250,7 +250,7 @@ def test_stringz_load_multibyte(): assert field.from_bytes(b"\xfe\xff\x00A\x00b\x00C\x00d\x00\x00") == "AbCd" -@pytest.mark.parametrize("null_value", (b"NULL\x00", "NULL")) +@pytest.mark.parametrize("null_value", [b"NULL\x00", "NULL"]) def test_stringz__load_null_value(null_value): field = stringlike.StringZ(null_value=null_value) assert field.from_bytes(b"NULL\x00") is None @@ -274,13 +274,13 @@ def test_stringz__load_with_default(): @pytest.mark.parametrize( - "storage_format,accessor_name", - ( + ("storage_format", "accessor_name"), + [ (stringlike.UUIDFormat.BINARY_VARIANT_1, "bytes"), (stringlike.UUIDFormat.BINARY_VARIANT_2, "bytes_le"), (stringlike.UUIDFormat.CANONICAL_STRING, ""), (stringlike.UUIDFormat.HEX_STRING, "hex"), - ), + ], ) def test_uuid_round_trip(storage_format, accessor_name): field = stringlike.UUID4(stored_as=storage_format) @@ -297,3 +297,9 @@ def test_uuid_round_trip(storage_format, accessor_name): loaded = field.from_bytes(serialized) assert loaded == value, "Deserialized value is wrong" + + +def test_uuid_short_read(): + field = stringlike.UUID4(stored_as=stringlike.UUIDFormat.CANONICAL_STRING) + with pytest.raises(errors.UnexpectedEOFError): + field.from_bytes(b"asdf") diff --git a/tests/full_examples/cpio_test.py b/tests/full_examples/cpio_test.py index 1ae9ea8a..e0ff0baa 100644 --- a/tests/full_examples/cpio_test.py +++ b/tests/full_examples/cpio_test.py @@ -42,7 +42,7 @@ def _file_size(self, all_fields): return len(all_fields["data"]) -@pytest.mark.parametrize("filename", ("evenlength.txt", "oddlength.txt")) +@pytest.mark.parametrize("filename", ["evenlength.txt", "oddlength.txt"]) def test_dump__padding_null_behaves(filename): filename_bytes = (filename + "\0").encode("utf-8") file_data = b"0123456789" @@ -51,7 +51,7 @@ def test_dump__padding_null_behaves(filename): struct = CPIOFileHeader(filename=filename, data=file_data, modified_time=when) serialized = struct.to_bytes() - assert serialized == ( + assert serialized == ( # noqa: ECE001 b"\xc7\x71" # Magic b"\x00\x00" # Device ID b"\x00\x00" # inumber @@ -69,13 +69,13 @@ def test_dump__padding_null_behaves(filename): ) -@pytest.mark.parametrize("filename", ("evenlength.txt", "oddlength.txt")) +@pytest.mark.parametrize("filename", ["evenlength.txt", "oddlength.txt"]) def test_load__padding_null_behaves(filename): filename_bytes = (filename + "\0").encode("utf-8") file_data = b"0123456789" when = datetime.datetime.fromtimestamp(0xBADF00D, datetime.timezone.utc) - serialized = ( + serialized = ( # noqa: ECE001 b"\xc7\x71" # Magic b"\x80\x00" # Device ID b"\xff\x7f" # inumber diff --git a/tests/full_examples/fat_test.py b/tests/full_examples/fat_test.py index 23a5c734..ca2beb41 100644 --- a/tests/full_examples/fat_test.py +++ b/tests/full_examples/fat_test.py @@ -24,7 +24,7 @@ class FAT12BootSector(binobj.Struct): drive_number = fields.UInt8() _reserved = fields.Bytes(const=b"\0", discard=True) _ex_boot_signature = fields.Bytes(const=b"\x29", discard=True) - volume_id = fields.UInt32(default=lambda: random.randrange(2**32)) + volume_id = fields.UInt32(factory=lambda: random.randrange(2**32)) volume_label = fields.String(size=11) fs_type = fields.String(size=8, default="FAT12", pad_byte=b" ", encoding="ascii") boot_code = fields.Bytes(size=448, default=b"\xcc" * 448) diff --git a/tests/helpers_test.py b/tests/helpers_test.py index c171d374..6f108650 100644 --- a/tests/helpers_test.py +++ b/tests/helpers_test.py @@ -18,7 +18,7 @@ def test_iter_bytes__zero(): assert stream.tell() == 0 -@pytest.mark.parametrize("endian", ("big", "little")) +@pytest.mark.parametrize("endian", ["big", "little"]) def test_write_int__endianness_default(monkeypatch, endian): """write_int should fall back to the system's endianness if it's not given. @@ -31,7 +31,7 @@ def test_write_int__endianness_default(monkeypatch, endian): assert stream.getvalue() == int.to_bytes(65432, 2, byteorder=endian, signed=False) -@pytest.mark.parametrize("start,expected", ((0, b"qwertyu"), (9, b"p{}"))) +@pytest.mark.parametrize(("start", "expected"), [(0, b"qwertyu"), (9, b"p{}")]) def test_peek_bytes__basic(start, expected): stream = io.BytesIO(b"qwertyuiop{}|") @@ -40,7 +40,7 @@ def test_peek_bytes__basic(start, expected): assert stream.tell() == start, "Stream position has moved." -@pytest.mark.parametrize("offset,expected", ((0, b""), (-3, b"{}|"))) +@pytest.mark.parametrize(("offset", "expected"), [(0, b""), (-3, b"{}|")]) def test_peek_bytes__short_read_okay_is_default(offset, expected): stream = io.BytesIO(b"qwertyuiop{}|") @@ -49,7 +49,7 @@ def test_peek_bytes__short_read_okay_is_default(offset, expected): assert stream.tell() == 13 + offset, "Stream position has moved." -@pytest.mark.parametrize("offset,expected", ((0, b"qwertyu"), (-3, b"[]|"))) +@pytest.mark.parametrize(("offset", "expected"), [(0, b"qwertyu"), (-3, b"[]|")]) def test_peek_bytes__short_read_crashes(offset, expected): """Throw EOFError if told to do so, and ensure that the stream pointer DOESN'T move on an error. diff --git a/tests/pep526_test.py b/tests/pep526_test.py index 8c0bb633..1feb8699 100644 --- a/tests/pep526_test.py +++ b/tests/pep526_test.py @@ -42,7 +42,7 @@ def test_field_extraction__field_properties_assigned(): assert BasicClass.string.encoding == "ibm500" -@pytest.mark.parametrize("field_type", (fields.StringZ, fields.UInt16)) +@pytest.mark.parametrize("field_type", [fields.StringZ, fields.UInt16]) def test_field_redefine_detected_crashes(field_type): with pytest.raises(errors.FieldRedefinedError): @@ -120,3 +120,12 @@ def test_passing_callable_triggers_warning(): @dataclass class _DeprecatedCallable(binobj.Struct): some_field: fields.Int32 = lambda: random.randrange(1024) + + +@pytest.mark.xfail() +def test_passing_callable_crashes(): + with pytest.raises(TypeError): + + @dataclass + class _DeprecatedCallable(binobj.Struct): + some_field: fields.Int32 = lambda: random.randrange(1024) diff --git a/tests/serialization_test.py b/tests/serialization_test.py index 17129ed3..c3fae793 100644 --- a/tests/serialization_test.py +++ b/tests/serialization_test.py @@ -43,11 +43,17 @@ def test_dump__use_default_value(): assert field.to_bytes() == b"\xde\xad\xbe\xef" -def test_dump__use_default_callable(): +def test_dump__use_default_callable_warns(): """Test dumping when the default value is a callable.""" with pytest.deprecated_call(): - field = fields.UInt32(name="field", default=lambda: 0x1234, endian="big") - assert field.to_bytes() == b"\x00\x00\x12\x34" + fields.UInt32(name="field", default=lambda: 0x1234, endian="big") + + +@pytest.mark.xfail() +def test_dump__use_default_callable_crashes(): + """Test dumping when the default value is a callable.""" + with pytest.raises(TypeError): + fields.UInt32(name="field", default=lambda: 0x1234, endian="big") def test_loads__extraneous_data_crashes(): @@ -147,11 +153,11 @@ def test_accessor__delitem__no_such_field(): @pytest.mark.parametrize( "instance", - ( + [ StructWithFieldOverrides(), StructWithFieldOverrides(one=1), StructWithFieldOverrides(one=1, two=2), - ), + ], ) def test_len__basic(instance): """Get the size of an instance with only fixed-length fields.""" diff --git a/tests/structures_test.py b/tests/structures_test.py index e17385b2..e7900af4 100644 --- a/tests/structures_test.py +++ b/tests/structures_test.py @@ -1,6 +1,7 @@ """Tests for the structure objects.""" import io +import re import pytest @@ -116,11 +117,11 @@ def test_partial_load__bad_column(): """Crash if an invalid column name is given.""" stream = io.BytesIO(b"zyxwvut\0\xba\xdc\x0f\xfe\xe1\x5b\xad\x01") - with pytest.raises(ValueError) as errinfo: + with pytest.raises( + ValueError, match="BasicStruct doesn't have a field named 'lol'." + ): BasicStruct.partial_load(stream, "lol") - assert str(errinfo.value) == "BasicStruct doesn't have a field named 'lol'." - def test_partial_load__short_read(): """A short read with no defined end field shouldn't crash partial_load().""" @@ -171,11 +172,11 @@ def test_get_field__basic(): def test_get_field__bad_name(): - with pytest.raises(ValueError) as errinfo: + with pytest.raises( + ValueError, match=re.escape("BasicStruct doesn't have a field named ':)'.") + ): BasicStruct.get_field(None, ":)") - assert str(errinfo.value) == "BasicStruct doesn't have a field named ':)'." - def test_dump__basic(): """Verify basic dump works.""" @@ -184,7 +185,7 @@ def test_dump__basic(): assert output == b"AbCdEfG\xff\xff\xff\xff\xff\xff\xff\x9c\xff\xff\0" -@pytest.mark.parametrize("extra_fields", ({"one"}, {"one", "two"})) +@pytest.mark.parametrize("extra_fields", [{"one"}, {"one", "two"}]) def test_dump__extra_fields(extra_fields): """Giving unrecognized fields will crash by default.""" with pytest.raises(errors.UnexpectedValueError) as errinfo: diff --git a/tests/varints_test.py b/tests/varints_test.py index 7f85d28e..46e69251 100644 --- a/tests/varints_test.py +++ b/tests/varints_test.py @@ -8,8 +8,8 @@ @pytest.mark.parametrize( - "value,expected", - ((0, b"\0"), (127, b"\x7f"), (128, b"\x81\x00"), (65535, b"\x83\xff\x7f")), + ("value", "expected"), + [(0, b"\0"), (127, b"\x7f"), (128, b"\x81\x00"), (65535, b"\x83\xff\x7f")], ) def test_encode_vlq_basic(value, expected): assert varints.encode_integer_vlq(value) == expected @@ -17,13 +17,15 @@ def test_encode_vlq_basic(value, expected): def test_encode_vlq_negative_crashes(): """Passing a negative value to the VLQ encoder must always fail.""" - with pytest.raises(ValueError): + with pytest.raises( + ValueError, match="The VLQ integer encoding doesn't support negative numbers." + ): varints.encode_integer_vlq(-1) @pytest.mark.parametrize( - "serialized,expected", - ((b"\0", 0), (b"\x7f", 127), (b"\x81\x00", 128), (b"\x83\xff\x7f", 65535)), + ("serialized", "expected"), + [(b"\0", 0), (b"\x7f", 127), (b"\x81\x00", 128), (b"\x83\xff\x7f", 65535)], ) def test_decode_vlq_basic(serialized, expected): buf = io.BytesIO(serialized) @@ -32,22 +34,22 @@ def test_decode_vlq_basic(serialized, expected): @pytest.mark.parametrize( - "value,expected", - ((0, b"\0"), (1, b"\x01"), (-32767, b"\xc1\xff\x7f"), (895484, b"\xb6\xd3\x7c")), + ("value", "expected"), + [(0, b"\0"), (1, b"\x01"), (-32767, b"\xc1\xff\x7f"), (895484, b"\xb6\xd3\x7c")], ) def test_encode_compact(value, expected): assert varints.encode_integer_compact(value) == expected @pytest.mark.parametrize( - "serialized,expected", - ( + ("serialized", "expected"), + [ (b"\0", 0), (b"\x01", 1), (b"\x41", -1), (b"\xc1\xff\x7f", -32767), (b"\xb6\xd3\x7c", 895484), - ), + ], ) def test_decode_compact(serialized, expected): buf = io.BytesIO(serialized) @@ -56,32 +58,35 @@ def test_decode_compact(serialized, expected): @pytest.mark.parametrize( - "value,expected", - ( + ("value", "expected"), + [ (0, b"\0"), (127, b"\x7f"), (128, b"\x80\x01"), (7345004, b"\xec\xa6\xc0\x03"), (0xB1ACC0FFEE2BAD, b"\xad\xd7\xb8\xff\x8f\x98\xeb\x58"), - ), + ], ) def test_encode_uleb128(value, expected): assert varints.encode_integer_uleb128(value) == expected def test_encode_uleb128__negative_crashes(): - with pytest.raises(ValueError): + with pytest.raises( + ValueError, + match="The ULEB128 integer encoding doesn't support negative numbers.", + ): assert varints.encode_integer_uleb128(-1) @pytest.mark.parametrize( - "serialized, expected", - ( + ("serialized", "expected"), + [ (b"\0", 0), (b"\x7f", 127), (b"\xd2\x85\xd8\xcc\x04", 1234567890), (b"\xad\xd7\xb8\xff\x8f\x98\xeb\x58", 0xB1ACC0FFEE2BAD), - ), + ], ) def test_decode_uleb128(serialized, expected): buf = io.BytesIO(serialized) @@ -104,12 +109,12 @@ def test_decode_uleb128(serialized, expected): ] -@pytest.mark.parametrize("value,expected", SIGNED_LEB_VALUES) +@pytest.mark.parametrize(("value", "expected"), SIGNED_LEB_VALUES) def test_encode_signed_leb128(value, expected): assert varints.encode_integer_leb128(value) == expected -@pytest.mark.parametrize("value,serialized", SIGNED_LEB_VALUES) +@pytest.mark.parametrize(("value", "serialized"), SIGNED_LEB_VALUES) def test_decode_signed_leb128(value, serialized): buf = io.BytesIO(serialized) assert varints.decode_integer_leb128(buf) == value diff --git a/tox.ini b/tox.ini index 220f6f2b..da0cecf9 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py37, py38, py39, py310, py311, pypy37, pypy38, pypy39, pypy310, lint, mypy +envlist = py37, py38, py39, py310, py311, py312, pypy37, pypy38, pypy39, pypy310, lint, mypy usedevelop = false skip_missing_interpreters = true setenv = @@ -65,7 +65,7 @@ exclude = max_line_length = 88 per_file_ignores = __init__.py:F403,F401,D104 - tests/*:D + tests/*:D,IF100,S101,S311 ; Plugin is broken, disabling for now ; unused_arguments_ignore_abstract_functions = true ; unused_arguments_ignore_dunder = true