From f47a9b2abadb503fed575f2967649face7f77555 Mon Sep 17 00:00:00 2001 From: Dave Shawley Date: Mon, 2 Nov 2015 14:21:11 -0500 Subject: [PATCH 1/8] tests: Correct msgpack-related test. This would fail if you inspected the response body :/ --- tests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests.py b/tests.py index dbea4b0..eddaeaa 100644 --- a/tests.py +++ b/tests.py @@ -35,7 +35,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=msgpack.packb({}), headers={'Content-Type': 'application/msgpack'}) self.assertEqual(response.code, 200) self.assertEqual(response.headers['Content-Type'], From ee26168e25bace2ab5b6219d18b9ca8b11784969 Mon Sep 17 00:00:00 2001 From: Dave Shawley Date: Mon, 2 Nov 2015 14:24:54 -0500 Subject: [PATCH 2/8] history.rst: Correct ReST syntax. --- docs/history.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/history.rst b/docs/history.rst index 7cd1220..2a90a48 100644 --- a/docs/history.rst +++ b/docs/history.rst @@ -2,12 +2,12 @@ Version History =============== `1.0.4`_ (14 Sep 2015) ---------------------- +---------------------- - Support using the default_content_type in the settings if request does not contain the Accept header `1.0.3`_ (10 Sep 2015) ---------------------- +---------------------- - Update installation files `1.0.2`_ (9 Sep 2015) From ce54332cab6c757c0db5209d445096e259a88091 Mon Sep 17 00:00:00 2001 From: Dave Shawley Date: Tue, 3 Nov 2015 08:10:08 -0500 Subject: [PATCH 3/8] Repackage sprockets.mixins.mediatype into a package. Distributing a raw python module into a namespace package seems to be somewhat unreliable though I haven't proven it yet... In any case, installing a package inside of a namespace package behaves itself. --- docs/history.rst | 6 + setup.py | 6 +- sprockets/mixins/mediatype/__init__.py | 24 ++++ .../{mediatype.py => mediatype/content.py} | 66 ++++------ sprockets/mixins/mediatype/handlers.py | 119 ++++++++++++++++++ 5 files changed, 179 insertions(+), 42 deletions(-) create mode 100644 sprockets/mixins/mediatype/__init__.py rename sprockets/mixins/{mediatype.py => mediatype/content.py} (84%) create mode 100644 sprockets/mixins/mediatype/handlers.py diff --git a/docs/history.rst b/docs/history.rst index 2a90a48..e71a345 100644 --- a/docs/history.rst +++ b/docs/history.rst @@ -1,6 +1,11 @@ Version History =============== +`Next Release`_ +--------------- +- Repackage from a module into a package. Distributing raw modules inside + of a namespace package is unreliable and questionably correct. + `1.0.4`_ (14 Sep 2015) ---------------------- - Support using the default_content_type in the settings if request does not @@ -22,6 +27,7 @@ Version History --------------------- - Initial Release +.. _Next Release: https://github.com/sprockets/sprockets.http/compare/1.0.4...HEAD .. _1.0.4: https://github.com/sprockets/sprockets.http/compare/1.0.3...1.0.4 .. _1.0.3: https://github.com/sprockets/sprockets.http/compare/1.0.2...1.0.3 .. _1.0.2: https://github.com/sprockets/sprockets.http/compare/1.0.1...1.0.2 diff --git a/setup.py b/setup.py index a7f374d..a4a5d2d 100755 --- a/setup.py +++ b/setup.py @@ -4,6 +4,8 @@ import os import setuptools +from sprockets.mixins import mediatype + def read_requirements(file_name): requirements = [] @@ -28,7 +30,7 @@ tests_require = read_requirements('testing.txt') setuptools.setup( name='sprockets.mixins.mediatype', - version='1.0.4', + version=mediatype.__version__, description='A mixin for reporting handling content-type/accept headers', long_description='\n' + open('README.rst').read(), url='https://github.com/sprockets/sprockets.mixins.media_type', @@ -36,7 +38,7 @@ setuptools.setup( author_email='api@aweber.com', license='BSD', classifiers=[ - 'Development Status :: 4 - Beta', + 'Development Status :: 5 - Production/Stable', 'Intended Audience :: Developers', 'License :: OSI Approved :: BSD License', 'Natural Language :: English', diff --git a/sprockets/mixins/mediatype/__init__.py b/sprockets/mixins/mediatype/__init__.py new file mode 100644 index 0000000..8e70e82 --- /dev/null +++ b/sprockets/mixins/mediatype/__init__.py @@ -0,0 +1,24 @@ +""" +sprockets.mixins.media_type + +""" +try: + from .content import (ContentMixin, ContentSettings, + add_binary_content_type, add_text_content_type, + set_default_content_type) + +except ImportError as error: # pragma no cover + def _error_closure(*args, **kwargs): + raise error + + ContentMixin = _error_closure + ContentSettings = _error_closure + add_binary_content_type = _error_closure + add_text_content_type = _error_closure + set_default_content_type = _error_closure + +version_info = (1, 0, 4) +__version__ = '.'.join(str(v) for v in version_info) +__all__ = ('ContentMixin', 'ContentSettings', 'add_binary_content_type', + 'add_text_content_type', 'set_default_content_type', + 'version_info', '__version__') diff --git a/sprockets/mixins/mediatype.py b/sprockets/mixins/mediatype/content.py similarity index 84% rename from sprockets/mixins/mediatype.py rename to sprockets/mixins/mediatype/content.py index 0a562d1..c0fbea5 100644 --- a/sprockets/mixins/mediatype.py +++ b/sprockets/mixins/mediatype/content.py @@ -1,16 +1,33 @@ """ -sprockets.mixins.media_type -=========================== +Content handling for Tornado. + +- :func:`.set_default_content_type` sets the content type that is + used when an ``Accept`` or ``Content-Type`` header is omitted. +- :func:`.add_binary_content_type` register transcoders for a binary + content type +- :func:`.add_text_content_type` register transcoders for a textual + content type +- :class:`.ContentSettings` an instance of this is attached to + :class:`tornado.web.Application` to hold the content mapping + information for the application +- :class:`.ContentMixin` attaches a :class:`.ContentSettings` + instance to the application and implements request decoding & + response encoding methods + +This module is the primary interface for this library. It exposes +functions for registering new content handlers and a mix-in that +adds content handling methods to :class:`~tornado.web.RequestHandler` +instances. """ import logging from ietfparse import algorithms, errors, headers -from tornado import escape, web +from tornado import web + +from . import handlers -version_info = (1, 0, 4) -__version__ = '.'.join(str(v) for v in version_info) logger = logging.getLogger(__name__) @@ -110,7 +127,8 @@ def add_binary_content_type(application, content_type, pack, unpack): """ settings = ContentSettings.from_application(application) - settings[content_type] = _BinaryContentHandler(content_type, pack, unpack) + settings[content_type] = handlers.BinaryContentHandler( + content_type, pack, unpack) def add_text_content_type(application, content_type, default_encoding, @@ -128,8 +146,8 @@ def add_text_content_type(application, content_type, default_encoding, """ settings = ContentSettings.from_application(application) - settings[content_type] = _TextContentHandler(content_type, dumps, loads, - default_encoding) + settings[content_type] = handlers.TextContentHandler( + content_type, dumps, loads, default_encoding) def set_default_content_type(application, content_type, encoding=None): @@ -232,35 +250,3 @@ class ContentMixin(object): if set_content_type: self.set_header('Content-Type', content_type) self.write(data_bytes) - - -class _BinaryContentHandler(object): - - def __init__(self, content_type, pack, unpack): - self._pack = pack - self._unpack = unpack - self.content_type = content_type - - def to_bytes(self, data_dict, encoding=None): - return self.content_type, self._pack(data_dict) - - def from_bytes(self, data, encoding=None): - return self._unpack(data) - - -class _TextContentHandler(object): - - def __init__(self, content_type, dumps, loads, default_encoding): - self._dumps = dumps - self._loads = loads - self.content_type = content_type - self.default_encoding = default_encoding - - def to_bytes(self, data_dict, encoding=None): - selected = encoding or self.default_encoding - content_type = '{0}; charset="{1}"'.format(self.content_type, selected) - dumped = self._dumps(escape.recursive_unicode(data_dict)) - return content_type, dumped.encode(selected) - - def from_bytes(self, data, encoding=None): - return self._loads(data.decode(encoding or self.default_encoding)) diff --git a/sprockets/mixins/mediatype/handlers.py b/sprockets/mixins/mediatype/handlers.py new file mode 100644 index 0000000..a4708ef --- /dev/null +++ b/sprockets/mixins/mediatype/handlers.py @@ -0,0 +1,119 @@ +""" +Basic content handlers. + +- :class:`BinaryContentHandler` basic transcoder for binary types that + simply calls functions for encoding and decoding +- :class:`TextContentHandler` transcoder that translates binary bodies + to text before calling functions that encode & decode text + +""" +from tornado import escape + + +class BinaryContentHandler(object): + """ + Pack and unpack binary types. + + :param str content_type: registered content type + :param pack: function that transforms an object instance + into :class:`bytes` + :param unpack: function that transforms :class:`bytes` + into an object instance + + This transcoder is a thin veneer around a pair of packing + and unpacking functions. + + """ + + def __init__(self, content_type, pack, unpack): + self._pack = pack + self._unpack = unpack + self.content_type = content_type + + def to_bytes(self, inst_data, encoding=None): + """ + Transform an object into :class:`bytes`. + + :param object inst_data: object to encode + :param str encoding: ignored + :returns: :class:`tuple` of the selected content + type and the :class:`bytes` representation of + `inst_data` + + """ + return self.content_type, self._pack(inst_data) + + def from_bytes(self, data_bytes, encoding=None): + """ + Get an object from :class:`bytes` + + :param bytes data_bytes: stream of bytes to decode + :param str encoding: ignored + :param dict content_parameters: optional :class:`dict` of + content type parameters from the :mailheader:`Content-Type` + header + :returns: decoded :class:`object` instance + + """ + return self._unpack(data_bytes) + + +class TextContentHandler(object): + """ + Transcodes between textual and object representations. + + :param str content_type: registered content type + :param dumps: function that transforms an object instance + into a :class:`str` + :param loads: function that transforms a :class:`str` + into an object instance + :param str default_encoding: encoding to apply when + transcoding from the underlying body :class:`byte` + instance + + This transcoder wraps functions that transcode between :class:`str` + and :class:`object` instances. In particular, it handles the + additional step of transcoding into the :class:`byte` instances + that tornado expects. + + """ + + def __init__(self, content_type, dumps, loads, default_encoding): + self._dumps = dumps + self._loads = loads + self.content_type = content_type + self.default_encoding = default_encoding + + def to_bytes(self, inst_data, encoding=None): + """ + Transform an object into :class:`bytes`. + + :param object inst_data: object to encode + :param str encoding: character set used to encode the bytes + returned from the ``dumps`` function. This defaults to + :attr:`default_encoding` + :returns: :class:`tuple` of the selected content + type and the :class:`bytes` representation of + `inst_data` + + """ + selected = encoding or self.default_encoding + content_type = '{0}; charset="{1}"'.format(self.content_type, selected) + dumped = self._dumps(escape.recursive_unicode(inst_data)) + return content_type, dumped.encode(selected) + + def from_bytes(self, data, encoding=None): + """ + Get an object from :class:`bytes` + + :param bytes data: stream of bytes to decode + :param str encoding: character set used to decode the incoming + bytes before calling the ``loads`` function. This defaults + to :attr:`default_encoding` + :param dict content_parameters: optional :class:`dict` of + content type parameters from the :mailheader:`Content-Type` + header + :returns: decoded :class:`object` instance + + """ + return self._loads(data.decode(encoding or self.default_encoding)) From f9cda82f643229362f24f4a83dabf09f9f4ff9cc Mon Sep 17 00:00:00 2001 From: Dave Shawley Date: Sun, 10 Jan 2016 10:44:53 -0500 Subject: [PATCH 4/8] Add deprecation warnings for top-level items. --- sprockets/mixins/mediatype/__init__.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/sprockets/mixins/mediatype/__init__.py b/sprockets/mixins/mediatype/__init__.py index 8e70e82..a61b2e5 100644 --- a/sprockets/mixins/mediatype/__init__.py +++ b/sprockets/mixins/mediatype/__init__.py @@ -2,6 +2,9 @@ sprockets.mixins.media_type """ +import functools +import warnings + try: from .content import (ContentMixin, ContentSettings, add_binary_content_type, add_text_content_type, @@ -17,6 +20,24 @@ except ImportError as error: # pragma no cover add_text_content_type = _error_closure set_default_content_type = _error_closure + +def _mark_deprecated(func): + msg = '{0}.{1} is deprecated, use {0}.content.{1} instead'.format( + 'sprockets.mixins.mediatype', func.__name__) + + @functools.wraps(func) + def wrapper(*args, **kwargs): + warnings.warn(msg, category=DeprecationWarning) + return func(*args, **kwargs) + + return wrapper + +add_binary_content_type = _mark_deprecated(add_binary_content_type) +add_text_content_type = _mark_deprecated(add_text_content_type) +set_default_content_type = _mark_deprecated(set_default_content_type) +ContentMixin = _mark_deprecated(ContentMixin) +ContentSettings = _mark_deprecated(ContentSettings) + version_info = (1, 0, 4) __version__ = '.'.join(str(v) for v in version_info) __all__ = ('ContentMixin', 'ContentSettings', 'add_binary_content_type', From 74ce03a0d55cea2560cd576c0617cd6a89b0f537 Mon Sep 17 00:00:00 2001 From: Dave Shawley Date: Tue, 3 Nov 2015 08:10:54 -0500 Subject: [PATCH 5/8] Update documentation & examples for new packaging. --- README.rst | 12 ++++++------ docs/api.rst | 2 +- examples.py | 16 ++++++++-------- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/README.rst b/README.rst index 9efaf24..c1a1b56 100644 --- a/README.rst +++ b/README.rst @@ -21,7 +21,7 @@ functions as parameters: import json - from sprockets.mixins import mediatype + from sprockets.mixins.mediatype import content from tornado import web def make_application(): @@ -29,9 +29,9 @@ functions as parameters: # insert your handlers here ]) - mediatype.add_text_content_type(application, - 'application/json', 'utf-8', - json.dumps, json.loads) + content.add_text_content_type(application, + 'application/json', 'utf-8', + json.dumps, json.loads) return application @@ -40,10 +40,10 @@ instance that the mix-in uses to manipulate the request and response bodies. .. code-block:: python - from sprockets.mixins import mediatype + from sprockets.mixins.mediatype import content from tornado import web - class SomeHandler(mediatype.ContentMixin, web.RequestHandler): + class SomeHandler(content.ContentMixin, web.RequestHandler): def get(self): self.send_response({'data': 'value'}) self.finish() diff --git a/docs/api.rst b/docs/api.rst index c55c9ac..bf98f6f 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -1,6 +1,6 @@ API Documentation ================= -.. currentmodule:: sprockets.mixins.mediatype +.. currentmodule:: sprockets.mixins.mediatype.content Content Type Handling --------------------- diff --git a/examples.py b/examples.py index 5ed56c0..4b558ae 100644 --- a/examples.py +++ b/examples.py @@ -2,12 +2,12 @@ import json import logging import signal -from sprockets.mixins import mediatype +from sprockets.mixins.mediatype import content from tornado import ioloop, web import msgpack -class SimpleHandler(mediatype.ContentMixin, web.RequestHandler): +class SimpleHandler(content.ContentMixin, web.RequestHandler): def post(self): body = self.get_request_body() @@ -18,12 +18,12 @@ class SimpleHandler(mediatype.ContentMixin, web.RequestHandler): def make_application(**settings): application = web.Application([web.url(r'/', SimpleHandler)], **settings) - mediatype.set_default_content_type(application, 'application/json', - encoding='utf-8') - mediatype.add_binary_content_type(application, 'application/msgpack', - msgpack.packb, msgpack.unpackb) - mediatype.add_text_content_type(application, 'application/json', 'utf-8', - json.dumps, json.loads) + 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_text_content_type(application, 'application/json', 'utf-8', + json.dumps, json.loads) return application From 51dcb9d4f5c3bc4aa6e6f6e8d7b5194c1035f722 Mon Sep 17 00:00:00 2001 From: Dave Shawley Date: Sun, 10 Jan 2016 11:09:29 -0500 Subject: [PATCH 6/8] Add Python3.5 to test coverage. --- .travis.yml | 1 + setup.cfg | 1 - tox.ini | 3 ++- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index b3008ed..3c4fb9d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,6 +2,7 @@ language: python python: - 2.7 - 3.4 + - 3.5 - pypy install: - pip install codecov diff --git a/setup.cfg b/setup.cfg index bc21099..9682f99 100644 --- a/setup.cfg +++ b/setup.cfg @@ -2,7 +2,6 @@ universal = 1 [nosetests] -with-coverage = 1 cover-branches = 1 cover-erase = 1 cover-package = sprockets.mixins diff --git a/tox.ini b/tox.ini index 71a7562..6871ecb 100644 --- a/tox.ini +++ b/tox.ini @@ -1,8 +1,9 @@ [tox] -envlist = py27,py34 +envlist = py27,py34,py35,pypy indexserver = default = https://pypi.python.org/simple toxworkdir = build/tox +skip_unknown_interpreters = true [testenv] deps = From b3126209e17fe007911853496086a68c91f74f64 Mon Sep 17 00:00:00 2001 From: Dave Shawley Date: Sun, 10 Jan 2016 13:49:38 -0500 Subject: [PATCH 7/8] Happy New Year, the 2016 edition. --- LICENSE | 2 +- docs/conf.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/LICENSE b/LICENSE index 3c72859..4a0e81a 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2015 AWeber Communications +Copyright (c) 2015-2016 AWeber Communications All rights reserved. Redistribution and use in source and binary forms, with or without modification, diff --git a/docs/conf.py b/docs/conf.py index f4cf61a..0b7d560 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -9,7 +9,7 @@ extensions = ['sphinx.ext.autodoc', source_suffix = '.rst' master_doc = 'index' project = 'sprockets.mixins.mediatype' -copyright = '2015, AWeber Communications' +copyright = '2015-2016, AWeber Communications' release = __version__ version = '.'.join(release.split('.')[0:1]) From 478868d5b4fee78402d9ddb7db31282200d12677 Mon Sep 17 00:00:00 2001 From: Dave Shawley Date: Sun, 10 Jan 2016 13:51:37 -0500 Subject: [PATCH 8/8] Remove unused docs/examples.rst. --- docs/examples.rst | 4 ---- 1 file changed, 4 deletions(-) delete mode 100644 docs/examples.rst diff --git a/docs/examples.rst b/docs/examples.rst deleted file mode 100644 index 0ac43f1..0000000 --- a/docs/examples.rst +++ /dev/null @@ -1,4 +0,0 @@ -Examples -======== - -.. literalinclude:: ../examples.py