Add s.m.mediatype.transcoders.MsgPackTranscoder.

This commit is contained in:
Dave Shawley 2016-01-13 07:28:44 -05:00
parent 8a44e527b1
commit 7d2237745e
6 changed files with 191 additions and 9 deletions

View file

@ -26,3 +26,6 @@ Bundled Transcoders
.. autoclass:: JSONTranscoder
:members:
.. autoclass:: MsgPackTranscoder
:members:

View file

@ -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)
----------------------

View file

@ -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

View file

@ -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

View file

@ -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__))

View file

@ -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())