Skip to content

Commit

Permalink
0.11.4 (#37)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
dargueta committed Mar 13, 2024
1 parent d5fe9bb commit 8733327
Show file tree
Hide file tree
Showing 29 changed files with 814 additions and 210 deletions.
6 changes: 4 additions & 2 deletions .github/workflows/ci.yml
Expand Up @@ -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"
Expand All @@ -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
Expand Down
4 changes: 1 addition & 3 deletions 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
Expand Down
2 changes: 1 addition & 1 deletion README.rst
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions binobj/__init__.py
Expand Up @@ -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 *
Expand All @@ -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()
9 changes: 4 additions & 5 deletions binobj/errors.py
Expand Up @@ -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)

Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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,
)

Expand Down
70 changes: 19 additions & 51 deletions binobj/fields/base.py
Expand Up @@ -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
Expand Down Expand Up @@ -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."""
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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(
Expand All @@ -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
Expand Down Expand Up @@ -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.
Expand All @@ -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.

Expand Down Expand Up @@ -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::
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
"""
Expand Down Expand Up @@ -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
Expand All @@ -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]
Expand All @@ -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:
Expand All @@ -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)

Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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:
Expand Down
20 changes: 12 additions & 8 deletions binobj/fields/containers.py
Expand Up @@ -130,15 +130,17 @@ 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,
)
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__
)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion binobj/fields/stringlike.py
Expand Up @@ -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."
Expand Down
10 changes: 5 additions & 5 deletions binobj/pep526.py
Expand Up @@ -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 (<py38)
from typing import get_args as get_typing_args
from typing import get_origin as get_typing_origin
except ImportError: # pragma: no cover (py38+)
Expand Down Expand Up @@ -148,7 +148,7 @@ def annotation_to_field_instance(
"""Convert a type annotation to a Field object if it represents one."""
if isinstance(annotation.type_class, type):
# We got a class object. Could be a struct or field, ignore everything else.
if issubclass(annotation.type_class, binobj.Struct):
if issubclass(annotation.type_class, Struct):
# A Struct class is shorthand for Nested(Struct).
return fields.Nested(annotation.type_class)
if issubclass(annotation.type_class, fields.Field):
Expand Down Expand Up @@ -239,7 +239,7 @@ class MyStruct(binobj.Struct):
# object. Otherwise, we'll end up with None or the default value provided:
#
# class MyStruct:
# foo: UInt8 = 123
# foo: UInt8 = 123 # noqa: E800
#
# If we don't do this `MyStruct.foo` will be `123`, not a Field object.
setattr(class_object, name, field_instance)
Expand Down

0 comments on commit 8733327

Please sign in to comment.