-
-
Notifications
You must be signed in to change notification settings - Fork 689
Implement new Bytes field
#2921
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: dev
Are you sure you want to change the base?
Changes from all commits
cf846bb
fc6cb24
e1b9376
a59efb5
9004ab1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,6 +2,7 @@ | |
| from __future__ import annotations | ||
|
|
||
| import abc | ||
| import base64 | ||
| import collections | ||
| import copy | ||
| import datetime as dt | ||
|
|
@@ -48,6 +49,7 @@ | |
| "AwareDateTime", | ||
| "Bool", | ||
| "Boolean", | ||
| "Bytes", | ||
| "Constant", | ||
| "Date", | ||
| "DateTime", | ||
|
|
@@ -878,6 +880,95 @@ def _deserialize(self, value, attr, data, **kwargs) -> str: | |
| raise self.make_error("invalid_utf8") from error | ||
|
|
||
|
|
||
| class Bytes(Field[bytes]): | ||
| """ | ||
| A field for deserializing strings into byte arrays. | ||
|
|
||
| :param encoding: Specifies the string encoding used when encoding/decoding to/from strings. | ||
| :param errors: Error behaviour when converting to/from a :class:`str`, inherited from it's constructor. | ||
| :param serialize: Specifies the return type when serializing. | ||
| `base64` and `str` use the value of `encoding` for the string. | ||
| :param kwargs: The same keyword arguments that :class:`Field` receives. | ||
|
|
||
| .. versionadded:: 4.3.0 | ||
| """ | ||
|
rrad5409 marked this conversation as resolved.
|
||
|
|
||
| #: Default error messages. | ||
| default_error_messages = { | ||
| "not_bytes": "Not a bytes-like object.", | ||
| "unicode": "Invalid unicode string.", | ||
| } | ||
|
|
||
| def __init__( | ||
| self, | ||
| encoding: str = "utf-8", | ||
| errors: str = "strict", | ||
| serialize: typing.Literal["int", "str", "bytes", "base64"] = "base64", | ||
| **kwargs: Unpack[_BaseFieldKwargs], | ||
| ): | ||
| super().__init__(**kwargs) | ||
| self.encoding = encoding | ||
| self.errors = errors | ||
| self.serialize = serialize | ||
|
|
||
| def _deserialize( | ||
| self, | ||
| value: typing.Any, | ||
| attr: str | None, | ||
| data: typing.Mapping[str, typing.Any] | None, | ||
| **kwargs: typing.Any, | ||
| ) -> bytes: | ||
| try: | ||
| match value: | ||
| case str() as s: | ||
| return bytes( | ||
| s, | ||
| encoding=self.encoding, | ||
| errors=self.errors, | ||
| ) | ||
| case int() as i: | ||
| return i.to_bytes( | ||
| length=max(1, (7 + i.bit_length()) // 8), | ||
| byteorder="big", | ||
| signed=i < 0, | ||
| ) | ||
|
Comment on lines
+929
to
+934
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Marshmallow often leans on the builtin constructor for type coercion to keep the implementation simple, then excludes types that might be confusing. if isinstance(value, (bool, int)):
raise ...
try:
return bytes(value)
except TypeError:
...Big int to bytes is probably out of scope.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The problem is that YAML doesn't natively support writing a Especially when writing binary-related values (like registers, addresses, bitmasks etc), writing it as The same reasoning applies to
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. i also lean towards omitting this. i plan to finish up and release field-level pre_load soon, which should simplify your use case here. def int_to_bytes(value):
if isinstance(value, int):
return value.to_bytes(
length=max(1, (7 + value.bit_length()) // 8),
byteorder="big",
signed=value < 0,
)
return value
class MySchema(Schema):
foo = fields.Bytes(pre_load=int_to_bytes)so to build off of @deckar01's suggestion, i think the final implementation should look something like: def _deserialize(self, value, attr, data, **kwargs) -> bytes:
if isinstance(value, (bool, int)):
raise self.make_error("invalid")
if isinstance(value, str):
try:
return value.encode("utf-8")
except UnicodeEncodeError as error:
raise self.make_error("invalid") from error
try:
return bytes(value)
except TypeError as error:
raise self.make_error("invalid") from error |
||
| case _: | ||
| return bytes(value) | ||
| except TypeError as e: | ||
| raise self.make_error("not_bytes") from e | ||
| except UnicodeError as e: | ||
| raise self.make_error("unicode") from e | ||
|
|
||
| def _serialize( | ||
| self, | ||
| value: bytes, | ||
| attr: str | None, | ||
| obj: typing.Any, | ||
| **kwargs: typing.Any, | ||
| ) -> str | int | bytes: | ||
| try: | ||
| match self.serialize: | ||
| case "str": | ||
| return str( | ||
| value, | ||
| encoding=self.encoding, | ||
| errors=self.errors, | ||
| ) | ||
| case "base64": | ||
| return base64.standard_b64encode(value) | ||
| case "int": | ||
| return int.from_bytes( | ||
| value, | ||
| byteorder="big", | ||
| ) | ||
| case "bytes": | ||
| return value | ||
| case _: | ||
| typing.assert_never(self.serialize) | ||
| except UnicodeError as e: | ||
| raise self.make_error("unicode") from e | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. serialization should not perform validation, since deserialized values are assumed to be valid. this was an explicit design choice. |
||
|
|
||
|
|
||
| class UUID(Field[uuid.UUID]): | ||
| """A UUID field.""" | ||
|
|
||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.