mirror of
https://github.com/sprockets/sprockets.mixins.mediatype.git
synced 2024-11-22 03:00:25 +00:00
Add s.m.mediatype.transcoders.MsgPackTranscoder.
This commit is contained in:
parent
8a44e527b1
commit
7d2237745e
6 changed files with 191 additions and 9 deletions
|
@ -26,3 +26,6 @@ Bundled Transcoders
|
|||
|
||||
.. autoclass:: JSONTranscoder
|
||||
:members:
|
||||
|
||||
.. autoclass:: MsgPackTranscoder
|
||||
:members:
|
||||
|
|
|
@ -7,6 +7,7 @@ Version History
|
|||
of a namespace package is unreliable and questionably correct.
|
||||
- Add :func:`sprockets.mixins.mediatype.content.add_transcoder`.
|
||||
- Add :class:`sprockets.mixins.mediatype.transcoders.JSONTranscoder`
|
||||
- Add :class:`sprockets.mixins.mediatype.transcoders.MsgPackTranscoder`
|
||||
|
||||
`1.0.4`_ (14 Sep 2015)
|
||||
----------------------
|
||||
|
|
|
@ -3,7 +3,6 @@ import signal
|
|||
|
||||
from sprockets.mixins.mediatype import content, transcoders
|
||||
from tornado import ioloop, web
|
||||
import msgpack
|
||||
|
||||
|
||||
class SimpleHandler(content.ContentMixin, web.RequestHandler):
|
||||
|
@ -19,8 +18,8 @@ def make_application(**settings):
|
|||
application = web.Application([web.url(r'/', SimpleHandler)], **settings)
|
||||
content.set_default_content_type(application, 'application/json',
|
||||
encoding='utf-8')
|
||||
content.add_binary_content_type(application, 'application/msgpack',
|
||||
msgpack.packb, msgpack.unpackb)
|
||||
content.add_transcoder(application, 'application/msgpack',
|
||||
transcoders.MsgPackTranscoder())
|
||||
content.add_transcoder(application, 'application/json',
|
||||
transcoders.JSONTranscoder())
|
||||
return application
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
coverage>=3.7,<3.99 # prevent installing 4.0b on ALL pip versions
|
||||
mock>=1.3,<2
|
||||
msgpack-python>=0.4,<0.5
|
||||
u-msgpack-python>=2,<3
|
||||
nose>=1.3,<2
|
||||
|
|
|
@ -2,12 +2,21 @@
|
|||
Bundled media type transcoders.
|
||||
|
||||
- :class:`.JSONTranscoder` implements JSON encoding/decoding
|
||||
- :class:`.MsgPackTranscoder` implements msgpack encoding/decoding
|
||||
|
||||
"""
|
||||
import base64
|
||||
import json
|
||||
import sys
|
||||
import uuid
|
||||
|
||||
import collections
|
||||
|
||||
try:
|
||||
import umsgpack
|
||||
except ImportError:
|
||||
umsgpack = None
|
||||
|
||||
from sprockets.mixins.mediatype import handlers
|
||||
|
||||
|
||||
|
@ -112,3 +121,117 @@ class JSONTranscoder(handlers.TextContentHandler):
|
|||
if isinstance(obj, (bytes, bytearray, memoryview)):
|
||||
return base64.b64encode(obj).decode('ASCII')
|
||||
raise TypeError('{!r} is not JSON serializable'.format(obj))
|
||||
|
||||
|
||||
class MsgPackTranscoder(handlers.BinaryContentHandler):
|
||||
"""
|
||||
Msgpack Transcoder instance.
|
||||
|
||||
:param str content_type: the content type that this encoder instance
|
||||
implements. If omitted, ``application/msgpack`` is used. This
|
||||
is passed directly to the ``BinaryContentHandler`` initializer.
|
||||
|
||||
This transcoder uses the `umsgpack`_ library to encode and decode
|
||||
objects according to the `msgpack`_ format.
|
||||
|
||||
.. _umsgpack: https://github.com/vsergeev/u-msgpack-python
|
||||
.. _msgpack: http://msgpack.org/index.html
|
||||
|
||||
"""
|
||||
if sys.version_info[0] < 3:
|
||||
PACKABLE_TYPES = (bool, int, float, long)
|
||||
else:
|
||||
PACKABLE_TYPES = (bool, int, float)
|
||||
|
||||
def __init__(self, content_type='application/msgpack'):
|
||||
if umsgpack is None:
|
||||
raise RuntimeError('Cannot import MsgPackTranscoder, '
|
||||
'umsgpack is not available')
|
||||
|
||||
super(MsgPackTranscoder, self).__init__(content_type, self.packb,
|
||||
self.unpackb)
|
||||
|
||||
def packb(self, data):
|
||||
"""Pack `data` into a :class:`bytes` instance."""
|
||||
return umsgpack.packb(self.normalize_datum(data))
|
||||
|
||||
def unpackb(self, data):
|
||||
"""Unpack a :class:`object` from a :class:`bytes` instance."""
|
||||
return umsgpack.unpackb(data)
|
||||
|
||||
def normalize_datum(self, datum):
|
||||
"""
|
||||
Convert `datum` into something that umsgpack likes.
|
||||
|
||||
:param datum: something that we want to process with umsgpack
|
||||
:return: a packable version of `datum`
|
||||
:raises TypeError: if `datum` cannot be packed
|
||||
|
||||
This message is called by :meth:`.packb` to recursively normalize
|
||||
an input value before passing it to :func:`umsgpack.packb`. Values
|
||||
are normalized according to the following table.
|
||||
|
||||
+-------------------------------+-------------------------------+
|
||||
| **Value** | **MsgPack Family** |
|
||||
+-------------------------------+-------------------------------+
|
||||
| :data:`None` | `nil byte`_ (0xC0) |
|
||||
+-------------------------------+-------------------------------+
|
||||
| :data:`True` | `true byte`_ (0xC3) |
|
||||
+-------------------------------+-------------------------------+
|
||||
| :data:`False` | `false byte`_ (0xC2) |
|
||||
+-------------------------------+-------------------------------+
|
||||
| :class:`int` | `integer family`_ |
|
||||
+-------------------------------+-------------------------------+
|
||||
| :class:`float` | `float family`_ |
|
||||
+-------------------------------+-------------------------------+
|
||||
| String | `str family`_ |
|
||||
+-------------------------------+-------------------------------+
|
||||
| :class:`collections.Sequence` | `array family`_ |
|
||||
+-------------------------------+-------------------------------+
|
||||
| :class:`collections.Set` | `array family`_ |
|
||||
+-------------------------------+-------------------------------+
|
||||
| :class:`collections.Mapping` | `map family`_ |
|
||||
+-------------------------------+-------------------------------+
|
||||
|
||||
.. _nil byte: https://github.com/msgpack/msgpack/blob/
|
||||
0b8f5ac67cdd130f4d4d4fe6afb839b989fdb86a/spec.md#formats-nil
|
||||
.. _true byte: https://github.com/msgpack/msgpack/blob/
|
||||
0b8f5ac67cdd130f4d4d4fe6afb839b989fdb86a/spec.md#bool-format-family
|
||||
.. _false byte: https://github.com/msgpack/msgpack/blob/
|
||||
0b8f5ac67cdd130f4d4d4fe6afb839b989fdb86a/spec.md#bool-format-family
|
||||
.. _integer family: https://github.com/msgpack/msgpack/blob/
|
||||
0b8f5ac67cdd130f4d4d4fe6afb839b989fdb86a/spec.md#int-format-family
|
||||
.. _float family: https://github.com/msgpack/msgpack/blob/
|
||||
0b8f5ac67cdd130f4d4d4fe6afb839b989fdb86a/spec.md#float-format-family
|
||||
.. _str family: https://github.com/msgpack/msgpack/blob/
|
||||
0b8f5ac67cdd130f4d4d4fe6afb839b989fdb86a/spec.md#str-format-family
|
||||
.. _array family: https://github.com/msgpack/msgpack/blob/
|
||||
0b8f5ac67cdd130f4d4d4fe6afb839b989fdb86a/spec.md#array-format-family
|
||||
.. _map family: https://github.com/msgpack/msgpack/blob/
|
||||
0b8f5ac67cdd130f4d4d4fe6afb839b989fdb86a/spec.md
|
||||
#mapping-format-family
|
||||
|
||||
"""
|
||||
if datum is None:
|
||||
return datum
|
||||
|
||||
if isinstance(datum, self.PACKABLE_TYPES):
|
||||
return datum
|
||||
|
||||
if isinstance(datum, str):
|
||||
return datum
|
||||
|
||||
if sys.version_info[0] < 3 and isinstance(datum, unicode):
|
||||
return datum
|
||||
|
||||
if isinstance(datum, (collections.Sequence, collections.Set)):
|
||||
return [self.normalize_datum(item) for item in datum]
|
||||
|
||||
if isinstance(datum, collections.Mapping):
|
||||
out = {}
|
||||
for k, v in datum.items():
|
||||
out[k] = self.normalize_datum(v)
|
||||
return out
|
||||
|
||||
raise TypeError(
|
||||
'{} is not msgpackable'.format(datum.__class__.__name__))
|
||||
|
|
66
tests.py
66
tests.py
|
@ -8,7 +8,7 @@ import unittest
|
|||
import uuid
|
||||
|
||||
from tornado import testing
|
||||
import msgpack
|
||||
import umsgpack
|
||||
|
||||
from sprockets.mixins.mediatype import content, handlers, transcoders
|
||||
import examples
|
||||
|
@ -28,6 +28,7 @@ class UTC(datetime.tzinfo):
|
|||
|
||||
|
||||
class Context(object):
|
||||
"""Super simple class to call setattr on"""
|
||||
pass
|
||||
|
||||
|
||||
|
@ -60,7 +61,7 @@ class SendResponseTests(testing.AsyncHTTPTestCase):
|
|||
'application/msgpack')
|
||||
|
||||
def test_that_default_content_type_is_set_on_response(self):
|
||||
response = self.fetch('/', method='POST', body=msgpack.packb({}),
|
||||
response = self.fetch('/', method='POST', body=umsgpack.packb({}),
|
||||
headers={'Content-Type': 'application/msgpack'})
|
||||
self.assertEqual(response.code, 200)
|
||||
self.assertEqual(response.headers['Content-Type'],
|
||||
|
@ -87,7 +88,7 @@ class GetRequestBodyTests(testing.AsyncHTTPTestCase):
|
|||
'utf8': u'\u2731'
|
||||
}
|
||||
}
|
||||
response = self.fetch('/', method='POST', body=msgpack.packb(body),
|
||||
response = self.fetch('/', method='POST', body=umsgpack.packb(body),
|
||||
headers={'Content-Type': 'application/msgpack'})
|
||||
self.assertEqual(response.code, 200)
|
||||
self.assertEqual(json.loads(response.body.decode('utf-8')), body)
|
||||
|
@ -144,8 +145,6 @@ class JSONTranscoderTests(unittest.TestCase):
|
|||
class ContentSettingsTests(unittest.TestCase):
|
||||
|
||||
def test_that_from_application_creates_instance(self):
|
||||
class Context(object):
|
||||
pass
|
||||
|
||||
context = Context()
|
||||
settings = content.ContentSettings.from_application(context)
|
||||
|
@ -192,3 +191,60 @@ class ContentFunctionTests(unittest.TestCase):
|
|||
self.assertIsInstance(transcoder, handlers.TextContentHandler)
|
||||
self.assertIs(transcoder._dumps, json.dumps)
|
||||
self.assertIs(transcoder._loads, json.loads)
|
||||
|
||||
|
||||
class MsgPackTranscoderTests(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(MsgPackTranscoderTests, self).setUp()
|
||||
self.transcoder = transcoders.MsgPackTranscoder()
|
||||
|
||||
def test_that_strings_are_dumped_as_strings(self):
|
||||
dumped = self.transcoder.packb(u'foo')
|
||||
self.assertEqual(self.transcoder.unpackb(dumped), 'foo')
|
||||
self.assertEqual(dumped, b'\xA3foo')
|
||||
|
||||
def test_that_none_is_packed_as_nil_byte(self):
|
||||
self.assertEqual(self.transcoder.packb(None), b'\xC0')
|
||||
|
||||
def test_that_bools_are_dumped_appropriately(self):
|
||||
self.assertEqual(self.transcoder.packb(False), b'\xC2')
|
||||
self.assertEqual(self.transcoder.packb(True), b'\xC3')
|
||||
|
||||
def test_that_ints_are_packed_appropriately(self):
|
||||
self.assertEqual(self.transcoder.packb((2 ** 7) - 1), b'\x7F')
|
||||
self.assertEqual(self.transcoder.packb(2 ** 7), b'\xCC\x80')
|
||||
self.assertEqual(self.transcoder.packb(2 ** 8), b'\xCD\x01\x00')
|
||||
self.assertEqual(self.transcoder.packb(2 ** 16),
|
||||
b'\xCE\x00\x01\x00\x00')
|
||||
self.assertEqual(self.transcoder.packb(2 ** 32),
|
||||
b'\xCF\x00\x00\x00\x01\x00\x00\x00\x00')
|
||||
|
||||
def test_that_negative_ints_are_packed_accordingly(self):
|
||||
self.assertEqual(self.transcoder.packb(-(2 ** 0)), b'\xFF')
|
||||
self.assertEqual(self.transcoder.packb(-(2 ** 5)), b'\xE0')
|
||||
self.assertEqual(self.transcoder.packb(-(2 ** 7)), b'\xD0\x80')
|
||||
self.assertEqual(self.transcoder.packb(-(2 ** 15)), b'\xD1\x80\x00')
|
||||
self.assertEqual(self.transcoder.packb(-(2 ** 31)),
|
||||
b'\xD2\x80\x00\x00\x00')
|
||||
self.assertEqual(self.transcoder.packb(-(2 ** 63)),
|
||||
b'\xD3\x80\x00\x00\x00\x00\x00\x00\x00')
|
||||
|
||||
def test_that_lists_are_treated_as_arrays(self):
|
||||
dumped = self.transcoder.packb(list())
|
||||
self.assertEqual(self.transcoder.unpackb(dumped), [])
|
||||
self.assertEqual(dumped, b'\x90')
|
||||
|
||||
def test_that_tuples_are_treated_as_arrays(self):
|
||||
dumped = self.transcoder.packb(tuple())
|
||||
self.assertEqual(self.transcoder.unpackb(dumped), [])
|
||||
self.assertEqual(dumped, b'\x90')
|
||||
|
||||
def test_that_sets_are_treated_as_arrays(self):
|
||||
dumped = self.transcoder.packb(set())
|
||||
self.assertEqual(self.transcoder.unpackb(dumped), [])
|
||||
self.assertEqual(dumped, b'\x90')
|
||||
|
||||
def test_that_unhandled_objects_raise_type_error(self):
|
||||
with self.assertRaises(TypeError):
|
||||
self.transcoder.packb(object())
|
||||
|
|
Loading…
Reference in a new issue