diff --git a/.gitignore b/.gitignore
index 643fde7..b3b30fe 100644
--- a/.gitignore
+++ b/.gitignore
@@ -14,6 +14,7 @@ downloads
eggs
fake-eggs
parts
+.eggs
# Packages #
############
diff --git a/.travis.yml b/.travis.yml
index 70da083..f2a76aa 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,23 +1,23 @@
language: python
+dist: xenial
python:
- - 2.7
- - 3.4
- 3.5
- 3.6
- - 3.7-dev
- - pypy
+ - 3.7
install:
- - pip install codecov
- - pip install -r requires/installation.txt
- - pip install -r requires/testing.txt
-script: nosetests
+ - pip install codecov -r requires/development.txt
+script:
+ - nosetests
+ - python setup.py build_sphinx
+ - python setup.py check
+ - flake8
after_success:
- codecov
deploy:
distributions: sdist bdist_wheel
provider: pypi
on:
- python: 2.7
+ python: 3.7
tags: true
all_branches: true
user: sprockets
diff --git a/LICENSE b/LICENSE
index 4a0e81a..4fa05e6 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,4 +1,4 @@
-Copyright (c) 2015-2016 AWeber Communications
+Copyright (c) 2015-2018 AWeber Communications
All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
diff --git a/README.rst b/README.rst
index 4f46851..659c94e 100644
--- a/README.rst
+++ b/README.rst
@@ -89,13 +89,11 @@ request handlers.
class SomeHandler(content.ContentMixin, web.RequestHandler):
def get(self):
self.send_response({'data': 'value'})
- self.finish()
def post(self):
body = self.get_request_body()
# do whatever
self.send_response({'action': 'performed'})
- self.finish()
Based on the settings stored in the ``Application`` instance and the HTTP
headers, the request and response data will be handled correctly or the
diff --git a/docs/api.rst b/docs/api.rst
index d78d06b..3374348 100644
--- a/docs/api.rst
+++ b/docs/api.rst
@@ -33,6 +33,3 @@ Bundled Transcoders
.. autoclass:: MsgPackTranscoder
:members:
-
-.. autoclass:: BinaryWrapper
- :members:
diff --git a/docs/conf.py b/docs/conf.py
index 391ad79..38e751e 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -1,23 +1,18 @@
-import alabaster
-from sprockets.mixins.mediatype import __version__
+import pkg_resources
needs_sphinx = '1.0'
extensions = ['sphinx.ext.autodoc',
'sphinx.ext.viewcode',
'sphinx.ext.intersphinx',
'sphinxcontrib.autohttp.tornado']
-source_suffix = '.rst'
master_doc = 'index'
project = 'sprockets.mixins.mediatype'
-copyright = '2015-2016, AWeber Communications'
-release = __version__
+copyright = '2015-2018, AWeber Communications'
+release = pkg_resources.get_distribution('sprockets.mixins.mediatype').version
version = '.'.join(release.split('.')[0:1])
-pygments_style = 'sphinx'
-html_theme = 'alabaster'
html_style = 'custom.css'
html_static_path = ['static']
-html_theme_path = [alabaster.get_path()]
html_sidebars = {
'**': ['about.html', 'navigation.html'],
}
diff --git a/examples.py b/examples.py
index bef1f27..354b94a 100644
--- a/examples.py
+++ b/examples.py
@@ -11,7 +11,6 @@ class SimpleHandler(content.ContentMixin, web.RequestHandler):
body = self.get_request_body()
self.set_status(200)
self.send_response(body)
- self.finish()
def make_application(**settings):
diff --git a/requires/development.txt b/requires/development.txt
index d49abfe..7c77917 100644
--- a/requires/development.txt
+++ b/requires/development.txt
@@ -1,4 +1,3 @@
+-e .[msgpack]
+-r docs.txt
-r testing.txt
--r installation.txt
-sphinx>=1.2,<2
-sphinxcontrib-httpdomain>=1.3,<2
diff --git a/requires/docs.txt b/requires/docs.txt
new file mode 100644
index 0000000..805b414
--- /dev/null
+++ b/requires/docs.txt
@@ -0,0 +1,2 @@
+sphinx==1.8.2
+sphinxcontrib-httpdomain==1.7.0
diff --git a/requires/installation.txt b/requires/installation.txt
index d450f7f..6dfc681 100644
--- a/requires/installation.txt
+++ b/requires/installation.txt
@@ -1,2 +1,2 @@
ietfparse>=1.4,<1.5
-tornado>=3.2,<6
+tornado>=5,<6
diff --git a/requires/testing.txt b/requires/testing.txt
index 9ce02c6..4f73836 100644
--- a/requires/testing.txt
+++ b/requires/testing.txt
@@ -1,4 +1,4 @@
-coverage>=3.7,<3.99 # prevent installing 4.0b on ALL pip versions
-mock>=1.3,<2
-u-msgpack-python>=2,<3
-nose>=1.3,<2
+coverage==4.5.2
+flake8==3.6.0
+nose==1.3.7
+tox==3.5.3
diff --git a/setup.cfg b/setup.cfg
index 9682f99..308548f 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,6 +1,13 @@
[bdist_wheel]
universal = 1
+[build_sphinx]
+fresh-env = 1
+warning-is-error = 1
+
+[check]
+strict = 1
+
[nosetests]
cover-branches = 1
cover-erase = 1
diff --git a/setup.py b/setup.py
index 47fa17a..1909304 100755
--- a/setup.py
+++ b/setup.py
@@ -1,38 +1,30 @@
#!/usr/bin/env python
#
-
-import os
+import pathlib
import setuptools
-from sprockets.mixins import mediatype
+
+REPO_DIR = pathlib.Path(__name__).parent
-def read_requirements(file_name):
+def read_requirements(name):
requirements = []
- try:
- with open(os.path.join('requires', file_name)) as req_file:
- for req_line in req_file:
- req_line = req_line.strip()
- if '#' in req_line:
- req_line = req_line[0:req_line.find('#')].strip()
- if req_line.startswith('-r'):
- req_line = req_line[2:].strip()
- requirements.extend(read_requirements(req_line))
- else:
- requirements.append(req_line)
- except IOError:
- pass
+ for req_line in REPO_DIR.joinpath(name).read_text().split('\n'):
+ req_line = req_line.strip()
+ if '#' in req_line:
+ req_line = req_line[0:req_line.find('#')].strip()
+ if req_line.startswith('-r'):
+ req_line = req_line[2:].strip()
+ requirements.extend(read_requirements(req_line))
+ else:
+ requirements.append(req_line)
return requirements
-install_requires = read_requirements('installation.txt')
-tests_require = read_requirements('testing.txt')
-
setuptools.setup(
name='sprockets.mixins.mediatype',
- version=mediatype.__version__,
description='A mixin for reporting handling content-type/accept headers',
- long_description='\n' + open('README.rst').read(),
+ long_description=REPO_DIR.joinpath('README.rst').read_text(),
url='https://github.com/sprockets/sprockets.mixins.media_type',
author='AWeber Communications',
author_email='api@aweber.com',
@@ -43,24 +35,27 @@ setuptools.setup(
'License :: OSI Approved :: BSD License',
'Natural Language :: English',
'Operating System :: OS Independent',
- 'Programming Language :: Python :: 2',
- 'Programming Language :: Python :: 2.6',
- 'Programming Language :: Python :: 2.7',
'Programming Language :: Python :: 3',
- 'Programming Language :: Python :: 3.2',
- 'Programming Language :: Python :: 3.3',
- 'Programming Language :: Python :: 3.4',
'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: 3.6',
'Programming Language :: Python :: 3.7',
'Programming Language :: Python :: Implementation :: CPython',
- 'Programming Language :: Python :: Implementation :: PyPy',
'Topic :: Software Development :: Libraries',
'Topic :: Software Development :: Libraries :: Python Modules'
],
- packages=setuptools.find_packages(),
- install_requires=install_requires,
- tests_require=tests_require,
+ packages=[
+ 'sprockets',
+ 'sprockets.mixins',
+ 'sprockets.mixins.mediatype'
+ ],
+ install_requires=read_requirements('requires/installation.txt'),
+ tests_require=read_requirements('requires/testing.txt'),
+ extras_require={
+ 'msgpack': ['u-msgpack-python>=2.5.0,<3']
+ },
+ setup_requires=['setuptools_scm'],
+ use_scm_version=True,
namespace_packages=['sprockets', 'sprockets.mixins'],
test_suite='nose.collector',
+ python_requires='>=3.5',
zip_safe=False)
diff --git a/sprockets/__init__.py b/sprockets/__init__.py
deleted file mode 100644
index de40ea7..0000000
--- a/sprockets/__init__.py
+++ /dev/null
@@ -1 +0,0 @@
-__import__('pkg_resources').declare_namespace(__name__)
diff --git a/sprockets/mixins/__init__.py b/sprockets/mixins/__init__.py
deleted file mode 100644
index de40ea7..0000000
--- a/sprockets/mixins/__init__.py
+++ /dev/null
@@ -1 +0,0 @@
-__import__('pkg_resources').declare_namespace(__name__)
diff --git a/sprockets/mixins/mediatype/__init__.py b/sprockets/mixins/mediatype/__init__.py
index 2acc914..7b32367 100644
--- a/sprockets/mixins/mediatype/__init__.py
+++ b/sprockets/mixins/mediatype/__init__.py
@@ -3,28 +3,11 @@ 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
-
- class ErrorClosureClass(object):
- def __init__(self, *args, **kwargs):
- raise error
-
- ContentMixin = ErrorClosureClass
- ContentSettings = ErrorClosureClass
- add_binary_content_type = _error_closure
- add_text_content_type = _error_closure
- set_default_content_type = _error_closure
+from .content import (ContentMixin, ContentSettings,
+ add_binary_content_type, add_text_content_type,
+ set_default_content_type)
-version_info = (2, 2, 2)
-__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/content.py b/sprockets/mixins/mediatype/content.py
index 0b5ec9d..e3d636c 100644
--- a/sprockets/mixins/mediatype/content.py
+++ b/sprockets/mixins/mediatype/content.py
@@ -43,7 +43,7 @@ SETTINGS_KEY = 'sprockets.mixins.mediatype.ContentSettings'
_warning_issued = False
-class ContentSettings(object):
+class ContentSettings:
"""
Content selection settings.
@@ -75,7 +75,6 @@ class ContentSettings(object):
response_body = settings['application/msgpack'].to_bytes(
response_dict, encoding='utf-8')
self.write(response_body)
- self.finish()
def make_application():
app = web.Application([web.url('/', SomeHandler)])
@@ -277,7 +276,7 @@ def set_default_content_type(application, content_type, encoding=None):
settings.default_encoding = encoding
-class ContentMixin(object):
+class ContentMixin:
"""
Mix this in to add some content handling methods.
@@ -288,7 +287,6 @@ class ContentMixin(object):
body = self.get_request_body()
# do stuff --> response_dict
self.send_response(response_dict)
- self.finish()
:meth:`get_request_body` will deserialize the request data into
a dictionary based on the :http:header:`Content-Type` request
@@ -300,7 +298,7 @@ class ContentMixin(object):
"""
def initialize(self):
- super(ContentMixin, self).initialize()
+ super().initialize()
self._request_body = None
self._best_response_match = None
self._logger = getattr(self, 'logger', logger)
diff --git a/sprockets/mixins/mediatype/handlers.py b/sprockets/mixins/mediatype/handlers.py
index a4708ef..ba1dee0 100644
--- a/sprockets/mixins/mediatype/handlers.py
+++ b/sprockets/mixins/mediatype/handlers.py
@@ -10,7 +10,7 @@ Basic content handlers.
from tornado import escape
-class BinaryContentHandler(object):
+class BinaryContentHandler:
"""
Pack and unpack binary types.
@@ -58,7 +58,7 @@ class BinaryContentHandler(object):
return self._unpack(data_bytes)
-class TextContentHandler(object):
+class TextContentHandler:
"""
Transcodes between textual and object representations.
diff --git a/sprockets/mixins/mediatype/transcoders.py b/sprockets/mixins/mediatype/transcoders.py
index 98291b6..a5123f0 100644
--- a/sprockets/mixins/mediatype/transcoders.py
+++ b/sprockets/mixins/mediatype/transcoders.py
@@ -7,7 +7,6 @@ Bundled media type transcoders.
"""
import base64
import json
-import sys
import uuid
import collections
@@ -20,28 +19,6 @@ except ImportError:
from sprockets.mixins.mediatype import handlers
-class BinaryWrapper(bytes):
- """
- Ensures that a Python 2 ``str`` is treated as binary.
-
- Since :class:`bytes` is a synonym for :class:`str` in Python 2,
- you cannot distinguish between something that should be binary
- and something that should be encoded as a string. This is a
- problem in formats `such as msgpack`_ where binary data and
- strings are encoded differently. The :class:`MsgPackTranscoder`
- accomodates this by trying to UTF-8 encode a :class:`str` instance
- and falling back to binary encoding if the transcode fails.
-
- You can avoid this by wrapping binary content in an instance of
- this class. The transcoder will then treat it as a binary payload
- instead of trying to detect whether it is a string or not.
-
- .. _such as msgpack: http://msgpack.org
-
- """
- pass
-
-
class JSONTranscoder(handlers.TextContentHandler):
"""
JSON transcoder instance.
@@ -73,8 +50,8 @@ class JSONTranscoder(handlers.TextContentHandler):
def __init__(self, content_type='application/json',
default_encoding='utf-8'):
- super(JSONTranscoder, self).__init__(content_type, self.dumps,
- self.loads, default_encoding)
+ super().__init__(content_type, self.dumps, self.loads,
+ default_encoding)
self.dump_options = {
'default': self.dump_object,
'separators': (',', ':'),
@@ -128,13 +105,6 @@ class JSONTranscoder(handlers.TextContentHandler):
| :class:`uuid.UUID` | Same as ``str(value)`` |
+----------------------------+---------------------------------------+
- .. warning::
-
- :class:`bytes` instances are treated as character strings by the
- standard JSON module in Python 2.7 so the *default* object hook
- is never called. In other words, :class:`bytes` values will not
- be serialized as Base64 strings in Python 2.7.
-
"""
if isinstance(obj, uuid.UUID):
return str(obj)
@@ -160,18 +130,14 @@ class MsgPackTranscoder(handlers.BinaryContentHandler):
.. _msgpack format: http://msgpack.org/index.html
"""
- if sys.version_info[0] < 3:
- PACKABLE_TYPES = (bool, int, float, long)
- else:
- PACKABLE_TYPES = (bool, int, float)
+ 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)
+ super().__init__(content_type, self.packb, self.unpackb)
def packb(self, data):
"""Pack `data` into a :class:`bytes` instance."""
@@ -206,7 +172,7 @@ class MsgPackTranscoder(handlers.BinaryContentHandler):
+-------------------------------+-------------------------------+
| :class:`float` | `float family`_ |
+-------------------------------+-------------------------------+
- | String (see note) | `str family`_ |
+ | String | `str family`_ |
+-------------------------------+-------------------------------+
| :class:`bytes` | `bin family`_ |
+-------------------------------+-------------------------------+
@@ -214,8 +180,6 @@ class MsgPackTranscoder(handlers.BinaryContentHandler):
+-------------------------------+-------------------------------+
| :class:`memoryview` | `bin family`_ |
+-------------------------------+-------------------------------+
- | :class:`.BinaryWrapper` | `bin family`_ |
- +-------------------------------+-------------------------------+
| :class:`collections.Sequence` | `array family`_ |
+-------------------------------+-------------------------------+
| :class:`collections.Set` | `array family`_ |
@@ -225,19 +189,6 @@ class MsgPackTranscoder(handlers.BinaryContentHandler):
| :class:`uuid.UUID` | Converted to String |
+-------------------------------+-------------------------------+
- .. note::
-
- :class:`str` and :class:`bytes` are the same before Python 3.
- If you want a value to be treated as a binary value, then you
- should wrap it in :class:`.BinaryWrapper` if there is any
- chance of running under Python 2.7.
-
- The processing of :class:`str` in Python 2.x attempts to
- encode the string as a UTF-8 stream. If the ``encode`` succeeds,
- then the string is encoded according to the `str family`_.
- If ``encode`` fails, then the string is encoded according to
- the `bin family`_ .
-
.. _nil byte: https://github.com/msgpack/msgpack/blob/
0b8f5ac67cdd130f4d4d4fe6afb839b989fdb86a/spec.md#formats-nil
.. _true byte: https://github.com/msgpack/msgpack/blob/
@@ -277,16 +228,6 @@ class MsgPackTranscoder(handlers.BinaryContentHandler):
if hasattr(datum, 'isoformat'):
datum = datum.isoformat()
- if sys.version_info[0] < 3 and isinstance(datum, (str, unicode)):
- if isinstance(datum, str) and not isinstance(datum, BinaryWrapper):
- # try to decode this into a string to make the common
- # case work. If we fail, then send along the bytes.
- try:
- datum = datum.decode('utf-8')
- except UnicodeDecodeError:
- pass
- return datum
-
if isinstance(datum, (bytes, str)):
return datum
diff --git a/tests.py b/tests.py
index aaae490..6f59077 100644
--- a/tests.py
+++ b/tests.py
@@ -4,7 +4,6 @@ import json
import os
import pickle
import struct
-import sys
import unittest
import uuid
@@ -28,7 +27,7 @@ class UTC(datetime.tzinfo):
return 'UTC'
-class Context(object):
+class Context:
"""Super simple class to call setattr on"""
def __init__(self):
self.settings = {}
@@ -37,27 +36,27 @@ class Context(object):
def pack_string(obj):
"""Optimally pack a string according to msgpack format"""
payload = str(obj).encode('ASCII')
- l = len(payload)
- if l < (2 ** 5):
- prefix = struct.pack('B', 0b10100000 | l)
- elif l < (2 ** 8):
- prefix = struct.pack('BB', 0xD9, l)
- elif l < (2 ** 16):
- prefix = struct.pack('>BH', 0xDA, l)
+ pl = len(payload)
+ if pl < (2 ** 5):
+ prefix = struct.pack('B', 0b10100000 | pl)
+ elif pl < (2 ** 8):
+ prefix = struct.pack('BB', 0xD9, pl)
+ elif pl < (2 ** 16):
+ prefix = struct.pack('>BH', 0xDA, pl)
else:
- prefix = struct.pack('>BI', 0xDB, l)
+ prefix = struct.pack('>BI', 0xDB, pl)
return prefix + payload
def pack_bytes(payload):
"""Optimally pack a byte string according to msgpack format"""
- l = len(payload)
- if l < (2 ** 8):
- prefix = struct.pack('BB', 0xC4, l)
- elif l < (2 ** 16):
- prefix = struct.pack('>BH', 0xC5, l)
+ pl = len(payload)
+ if pl < (2 ** 8):
+ prefix = struct.pack('BB', 0xC4, pl)
+ elif pl < (2 ** 16):
+ prefix = struct.pack('>BH', 0xC5, pl)
else:
- prefix = struct.pack('>BI', 0xC6, l)
+ prefix = struct.pack('>BI', 0xC6, pl)
return prefix + payload
@@ -111,16 +110,16 @@ class GetRequestBodyTests(testing.AsyncHTTPTestCase):
def test_that_request_with_unhandled_type_results_in_415(self):
response = self.fetch(
'/', method='POST', headers={'Content-Type': 'application/xml'},
- body=(u'value'
- u'\u2731'
- u'').encode('utf-8'))
+ body=('value'
+ '\u2731'
+ '').encode('utf-8'))
self.assertEqual(response.code, 415)
def test_that_msgpack_request_returns_default_type(self):
body = {
'name': 'value',
'embedded': {
- 'utf8': u'\u2731'
+ 'utf8': '\u2731'
}
}
response = self.fetch('/', method='POST', body=umsgpack.packb(body),
@@ -140,7 +139,7 @@ class GetRequestBodyTests(testing.AsyncHTTPTestCase):
class JSONTranscoderTests(unittest.TestCase):
def setUp(self):
- super(JSONTranscoderTests, self).setUp()
+ super().setUp()
self.transcoder = transcoders.JSONTranscoder()
def test_that_uuids_are_dumped_as_strings(self):
@@ -161,13 +160,6 @@ class JSONTranscoderTests(unittest.TestCase):
self.assertEqual(dumped.replace(' ', ''),
'{"now":"%s"}' % obj['now'].isoformat())
- @unittest.skipIf(sys.version_info[0] == 2, 'bytes unsupported on python 2')
- def test_that_bytes_are_base64_encoded(self):
- bin = bytes(os.urandom(127))
- dumped = self.transcoder.dumps({'bin': bin})
- self.assertEqual(
- dumped, '{"bin":"%s"}' % base64.b64encode(bin).decode('ASCII'))
-
def test_that_bytearrays_are_base64_encoded(self):
bin = bytearray(os.urandom(127))
dumped = self.transcoder.dumps({'bin': bin})
@@ -226,7 +218,7 @@ class ContentSettingsTests(unittest.TestCase):
class ContentFunctionTests(unittest.TestCase):
def setUp(self):
- super(ContentFunctionTests, self).setUp()
+ super().setUp()
self.context = Context()
def test_that_add_binary_content_type_creates_binary_handler(self):
@@ -281,11 +273,11 @@ class ContentFunctionTests(unittest.TestCase):
class MsgPackTranscoderTests(unittest.TestCase):
def setUp(self):
- super(MsgPackTranscoderTests, self).setUp()
+ super().setUp()
self.transcoder = transcoders.MsgPackTranscoder()
def test_that_strings_are_dumped_as_strings(self):
- dumped = self.transcoder.packb(u'foo')
+ dumped = self.transcoder.packb('foo')
self.assertEqual(self.transcoder.unpackb(dumped), 'foo')
self.assertEqual(dumped, pack_string('foo'))
@@ -373,6 +365,6 @@ class MsgPackTranscoderTests(unittest.TestCase):
def test_that_utf8_values_can_be_forced_to_bytes(self):
data = b'a ascii value'
- dumped = self.transcoder.packb(transcoders.BinaryWrapper(data))
+ dumped = self.transcoder.packb(data)
self.assertEqual(self.transcoder.unpackb(dumped), data)
self.assertEqual(dumped, pack_bytes(data))
diff --git a/tox.ini b/tox.ini
index a8ff895..884e23c 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,5 +1,5 @@
[tox]
-envlist = py27,py34,py35,pypy
+envlist = py35,py36,py37
indexserver =
default = https://pypi.python.org/simple
toxworkdir = build/tox
@@ -7,6 +7,6 @@ skip_missing_interpreters = true
[testenv]
deps =
- -rrequires/installation.txt
- -rrequires/testing.txt
+ -e .[msgpack]
+ -r requires/testing.txt
commands = nosetests []