mirror of
https://github.com/sprockets/sprockets.mixins.mediatype.git
synced 2025-01-01 11:13:21 +00:00
Add support for encoding dataclasses.
This commit is contained in:
parent
be5eb94cbf
commit
1fb8dff9e2
5 changed files with 74 additions and 1 deletions
|
@ -99,3 +99,6 @@ Contract Types
|
|||
|
||||
.. autoclass:: SupportsSettings
|
||||
:members:
|
||||
|
||||
.. autoclass:: SupportsDataclassFields
|
||||
:members:
|
||||
|
|
|
@ -5,6 +5,7 @@ Version History
|
|||
--------------------------------
|
||||
- Add a transcoder for `application/x-www-formurlencoded`_
|
||||
- Add support for encoding :class:`decimal.Decimal`
|
||||
- Add support for encoding :func:`dataclasses.dataclass` decorated classes
|
||||
- Add type annotations (see :ref:`type-info`)
|
||||
- Return a "406 Not Acceptable" if the :http:header:`Accept` header values cannot be matched
|
||||
and there is no default content type configured
|
||||
|
|
|
@ -115,6 +115,8 @@ class JSONTranscoder(handlers.TextContentHandler):
|
|||
+----------------------------+---------------------------------------+
|
||||
| :class:`decimal.Decimal` | Same as ``float(value)`` |
|
||||
+----------------------------+---------------------------------------+
|
||||
| Dataclasses | Same as :func:`dataclasses.asdict` |
|
||||
+----------------------------+---------------------------------------+
|
||||
|
||||
"""
|
||||
if isinstance(obj, uuid.UUID):
|
||||
|
@ -125,6 +127,8 @@ class JSONTranscoder(handlers.TextContentHandler):
|
|||
return base64.b64encode(obj).decode('ASCII')
|
||||
if isinstance(obj, decimal.Decimal):
|
||||
return float(obj)
|
||||
if dataclasses.is_dataclass(obj):
|
||||
return dataclasses.asdict(obj)
|
||||
raise TypeError('{!r} is not JSON serializable'.format(obj))
|
||||
|
||||
|
||||
|
@ -204,6 +208,9 @@ class MsgPackTranscoder(handlers.BinaryContentHandler):
|
|||
+-----------------------------------+-------------------------------+
|
||||
| :class:`decimal.Decimal` | `float family`_ |
|
||||
+-----------------------------------+-------------------------------+
|
||||
| Dataclasses | `map family`_ after calling |
|
||||
| | :func:`dataclasses.asdict` |
|
||||
+-----------------------------------+-------------------------------+
|
||||
|
||||
.. _nil byte: https://github.com/msgpack/msgpack/blob/
|
||||
0b8f5ac67cdd130f4d4d4fe6afb839b989fdb86a/spec.md#formats-nil
|
||||
|
@ -253,6 +260,9 @@ class MsgPackTranscoder(handlers.BinaryContentHandler):
|
|||
if isinstance(datum, (collections.abc.Sequence, collections.abc.Set)):
|
||||
return [self.normalize_datum(item) for item in datum]
|
||||
|
||||
if dataclasses.is_dataclass(datum):
|
||||
datum = dataclasses.asdict(datum)
|
||||
|
||||
if isinstance(datum, collections.abc.Mapping):
|
||||
out = {}
|
||||
for k, v in datum.items():
|
||||
|
@ -319,6 +329,9 @@ class FormUrlEncodedTranscoder:
|
|||
| :class:`datetime.datetime` | result of calling |
|
||||
| | :meth:`~datetime.datetime.isoformat` |
|
||||
+----------------------------+---------------------------------------+
|
||||
| Dataclasses | same as calling |
|
||||
| | :func:`dataclasses.asdict` on value |
|
||||
+----------------------------+---------------------------------------+
|
||||
|
||||
https://url.spec.whatwg.org/#application/x-www-form-urlencoded
|
||||
|
||||
|
@ -465,6 +478,9 @@ class FormUrlEncodedTranscoder:
|
|||
self, value: type_info.Serializable
|
||||
) -> typing.Iterable[typing.Tuple[typing.Any, typing.Any]]:
|
||||
tuples: typing.Iterable[typing.Tuple[typing.Any, typing.Any]]
|
||||
if dataclasses.is_dataclass(value):
|
||||
value = dataclasses.asdict(value)
|
||||
|
||||
if isinstance(value, collections.abc.Mapping):
|
||||
tuples = value.items()
|
||||
else:
|
||||
|
|
|
@ -12,6 +12,17 @@ except ImportError:
|
|||
from typing_extensions import Protocol, runtime_checkable # type: ignore
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class SupportsDataclassFields(Protocol):
|
||||
"""An object that looks like a dataclass.
|
||||
|
||||
The implementation uses the same test that :func:`dataclasses.is_dataclass`
|
||||
uses in Python 3.9.
|
||||
|
||||
"""
|
||||
__dataclass_fields__: typing.ClassVar[typing.Mapping[str, typing.Any]]
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class SupportsIsoFormat(Protocol):
|
||||
"""An object that has an isoformat method."""
|
||||
|
@ -29,7 +40,7 @@ class SupportsSettings(Protocol):
|
|||
Serializable = typing.Union[SupportsIsoFormat, None, bool, bytearray, bytes,
|
||||
float, int, memoryview, str, typing.Mapping,
|
||||
typing.Sequence, typing.Set, uuid.UUID,
|
||||
decimal.Decimal]
|
||||
decimal.Decimal, SupportsDataclassFields]
|
||||
"""Types that can be serialized by this library.
|
||||
|
||||
This is the set of types that
|
||||
|
|
42
tests.py
42
tests.py
|
@ -1,4 +1,5 @@
|
|||
import base64
|
||||
import dataclasses
|
||||
import datetime
|
||||
import decimal
|
||||
import json
|
||||
|
@ -354,6 +355,17 @@ class JSONTranscoderTests(unittest.TestCase):
|
|||
loaded = json.loads(dumped)
|
||||
self.assertEqual(loaded['n'], float(pi))
|
||||
|
||||
def test_that_dataclasses_are_handled_as_dicts(self):
|
||||
@dataclasses.dataclass
|
||||
class Point:
|
||||
x: int
|
||||
y: int
|
||||
|
||||
datum = Point(3, 4)
|
||||
dumped = self.transcoder.dumps({'point': datum})
|
||||
expected = json.dumps({'point': dataclasses.asdict(datum)})
|
||||
self.assertDictEqual(json.loads(expected), json.loads(dumped))
|
||||
|
||||
|
||||
class ContentSettingsTests(unittest.TestCase):
|
||||
def test_that_handler_listed_in_available_content_types(self):
|
||||
|
@ -566,6 +578,26 @@ class MsgPackTranscoderTests(unittest.TestCase):
|
|||
self.assertEqual(0xcb, dumped[0])
|
||||
self.assertEqual(struct.pack('>d', float(pi)), dumped[1:])
|
||||
|
||||
def test_that_dataclasses_are_handled_as_dicts(self):
|
||||
@dataclasses.dataclass
|
||||
class Point:
|
||||
x: int
|
||||
y: int
|
||||
|
||||
datum = typing.cast(type_info.SupportsDataclassFields, Point(3, 4))
|
||||
dumped = self.transcoder.packb(datum)
|
||||
expected = struct.pack(
|
||||
'BBBBBBB',
|
||||
0x82, # mapping of two fields
|
||||
0xA1, # string of a single byte
|
||||
ord('x'),
|
||||
3, # positive integer less than 128
|
||||
0xA1, # string of a single byte
|
||||
ord('y'),
|
||||
4, # positive integer less than 128
|
||||
)
|
||||
self.assertEqual(expected, dumped)
|
||||
|
||||
|
||||
class FormUrlEncodingTranscoderTests(unittest.TestCase):
|
||||
transcoder: type_info.Transcoder
|
||||
|
@ -715,3 +747,13 @@ class FormUrlEncodingTranscoderTests(unittest.TestCase):
|
|||
pi = decimal.Decimal('3.142857142857142857142857143')
|
||||
_, result = self.transcoder.to_bytes({'pi': pi})
|
||||
self.assertEqual('pi={}'.format(str(pi)).encode(), result)
|
||||
|
||||
def test_that_dataclasses_are_handled_as_dicts(self):
|
||||
@dataclasses.dataclass
|
||||
class Point:
|
||||
x: int
|
||||
y: int
|
||||
|
||||
datum = typing.cast(type_info.SupportsDataclassFields, Point(3, 4))
|
||||
_, result = self.transcoder.to_bytes(datum)
|
||||
self.assertEqual(b'x=3&y=4', result)
|
||||
|
|
Loading…
Reference in a new issue