diff --git a/.travis.yml b/.travis.yml index af709cd..f132d06 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,6 +5,8 @@ python: - "2.7" - "3.3" - "3.4" + - "3.5" + - "3.6" install: - pip install -r requirements.txt diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..e92b342 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,29 @@ +# License + +Copyright © 2017-present, Tom Christie. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/coreapi/__init__.py b/coreapi/__init__.py index 5f069ba..92ac890 100644 --- a/coreapi/__init__.py +++ b/coreapi/__init__.py @@ -4,7 +4,7 @@ from coreapi.document import Array, Document, Link, Object, Error, Field -__version__ = '2.3.0' +__version__ = '2.3.3' __all__ = [ 'Array', 'Document', 'Link', 'Object', 'Error', 'Field', 'Client', diff --git a/coreapi/client.py b/coreapi/client.py index d02a59c..00b0057 100644 --- a/coreapi/client.py +++ b/coreapi/client.py @@ -105,7 +105,7 @@ def __init__(self, decoders=None, transports=None, auth=None, session=None): if decoders is None: decoders = get_default_decoders() if transports is None: - transports = get_default_transports(auth=auth) + transports = get_default_transports(auth=auth, session=session) self._decoders = itypes.List(decoders) self._transports = itypes.List(transports) diff --git a/coreapi/codecs/corejson.py b/coreapi/codecs/corejson.py index 5261618..f025533 100644 --- a/coreapi/codecs/corejson.py +++ b/coreapi/codecs/corejson.py @@ -32,20 +32,31 @@ def encode_schema_to_corejson(schema): - type_id = SCHEMA_CLASS_TO_TYPE_ID.get(schema.__class__, 'anything') - return { + if hasattr(schema, 'typename'): + type_id = schema.typename + else: + type_id = SCHEMA_CLASS_TO_TYPE_ID.get(schema.__class__, 'anything') + retval = { '_type': type_id, 'title': schema.title, 'description': schema.description } + if hasattr(schema, 'enum'): + retval['enum'] = schema.enum + return retval def decode_schema_from_corejson(data): type_id = _get_string(data, '_type') title = _get_string(data, 'title') description = _get_string(data, 'description') + + kwargs = {} + if type_id == 'enum': + kwargs['enum'] = _get_list(data, 'enum') + schema_cls = TYPE_ID_TO_SCHEMA_CLASS.get(type_id, coreschema.Anything) - return schema_cls(title=title, description=description) + return schema_cls(title=title, description=description, **kwargs) # Robust dictionary lookups, that always return an item of the correct @@ -128,15 +139,15 @@ def _get_content(item, base_url=None): Return a dictionary of content, for documents, objects and errors. """ return { - _unescape_key(key): _primative_to_document(value, base_url) + _unescape_key(key): _primitive_to_document(value, base_url) for key, value in item.items() if key not in ('_type', '_meta') } -def _document_to_primative(node, base_url=None): +def _document_to_primitive(node, base_url=None): """ - Take a Core API document and return Python primatives + Take a Core API document and return Python primitives ready to be rendered into the JSON style encoding. """ if isinstance(node, Document): @@ -156,7 +167,7 @@ def _document_to_primative(node, base_url=None): # Fill in key-value content. ret.update([ - (_escape_key(key), _document_to_primative(value, base_url=url)) + (_escape_key(key), _document_to_primitive(value, base_url=url)) for key, value in node.items() ]) return ret @@ -170,7 +181,7 @@ def _document_to_primative(node, base_url=None): # Fill in key-value content. ret.update([ - (_escape_key(key), _document_to_primative(value, base_url=base_url)) + (_escape_key(key), _document_to_primitive(value, base_url=base_url)) for key, value in node.items() ]) return ret @@ -193,7 +204,7 @@ def _document_to_primative(node, base_url=None): ret['description'] = node.description if node.fields: ret['fields'] = [ - _document_to_primative(field) for field in node.fields + _document_to_primitive(field) for field in node.fields ] return ret @@ -209,19 +220,19 @@ def _document_to_primative(node, base_url=None): elif isinstance(node, Object): return OrderedDict([ - (_escape_key(key), _document_to_primative(value, base_url=base_url)) + (_escape_key(key), _document_to_primitive(value, base_url=base_url)) for key, value in node.items() ]) elif isinstance(node, Array): - return [_document_to_primative(value) for value in node] + return [_document_to_primitive(value) for value in node] return node -def _primative_to_document(data, base_url=None): +def _primitive_to_document(data, base_url=None): """ - Take Python primatives as returned from parsing JSON content, + Take Python primitives as returned from parsing JSON content, and return a Core API document. """ if isinstance(data, dict) and data.get('_type') == 'document': @@ -278,7 +289,7 @@ def _primative_to_document(data, base_url=None): elif isinstance(data, list): # Array - content = [_primative_to_document(item, base_url) for item in data] + content = [_primitive_to_document(item, base_url) for item in data] return Array(content) # String, Integer, Number, Boolean, null. @@ -303,7 +314,7 @@ def decode(self, bytestring, **options): except ValueError as exc: raise ParseError('Malformed JSON. %s' % exc) - doc = _primative_to_document(data, base_url) + doc = _primitive_to_document(data, base_url) if isinstance(doc, Object): doc = Document(content=dict(doc)) @@ -331,5 +342,5 @@ def encode(self, document, **options): 'separators': COMPACT_SEPARATORS } - data = _document_to_primative(document) + data = _document_to_primitive(document) return force_bytes(json.dumps(data, **kwargs)) diff --git a/coreapi/document.py b/coreapi/document.py index da9044a..c6c9ceb 100644 --- a/coreapi/document.py +++ b/coreapi/document.py @@ -44,13 +44,13 @@ def _key_sorting(item): # The field class, as used by Link objects: -# NOTE: 'type', 'description' and 'examaple' are now deprecated, +# NOTE: 'type', 'description' and 'example' are now deprecated, # in favor of 'schema'. Field = namedtuple('Field', ['name', 'required', 'location', 'schema', 'description', 'type', 'example']) Field.__new__.__defaults__ = (False, '', None, None, None, None) -# The Core API primatives: +# The Core API primitives: class Document(itypes.Dict): """ diff --git a/coreapi/transports/http.py b/coreapi/transports/http.py index a548024..7338e61 100644 --- a/coreapi/transports/http.py +++ b/coreapi/transports/http.py @@ -376,7 +376,8 @@ def transition(self, link, decoders, params=None, link_ancestors=None, force_cod headers.update(self.headers) request = _build_http_request(session, url, method, headers, encoding, params) - response = session.send(request) + settings = session.merge_environment_settings(request.url, None, None, None, None) + response = session.send(request, **settings) result = _decode_result(response, decoders, force_codec) if isinstance(result, Document) and link_ancestors: diff --git a/coreapi/utils.py b/coreapi/utils.py index 4283583..fb7ade4 100644 --- a/coreapi/utils.py +++ b/coreapi/utils.py @@ -237,7 +237,7 @@ def negotiate_encoder(encoders, accept=None): raise exceptions.NoCodecAvailable(msg) -# Validation utilities. Used to ensure that we get consitent validation +# Validation utilities. Used to ensure that we get consistent validation # exceptions when invalid types are passed as a parameter, rather than # an exception occuring when the request is made. @@ -314,7 +314,7 @@ def _validate_form_field(value, allow_files=False, allow_list=True): elif allow_files and is_file(value): return value - msg = 'Must be a primative type.' + msg = 'Must be a primitive type.' raise exceptions.ParameterError(msg) @@ -332,5 +332,5 @@ def _validate_json_data(value): for item_key, item_val in value.items() } - msg = 'Must be a JSON primative.' + msg = 'Must be a JSON primitive.' raise exceptions.ParameterError(msg) diff --git a/runtests b/runtests index 54a984d..8b260d9 100755 --- a/runtests +++ b/runtests @@ -35,10 +35,10 @@ def flake8_main(args): def report_coverage(cov, fail_if_not_100=False): - precent_covered = cov.report( + percent_covered = cov.report( file=NullFile(), **COVERAGE_OPTIONS ) - if precent_covered == 100: + if percent_covered == 100: print('100% coverage') return if fail_if_not_100: diff --git a/setup.py b/setup.py index ef91e7f..7dabead 100755 --- a/setup.py +++ b/setup.py @@ -86,6 +86,13 @@ def get_package_data(package): 'License :: OSI Approved :: BSD License', 'Operating System :: OS Independent', 'Programming Language :: Python', + 'Programming Language :: Python :: 2', + 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.3', + 'Programming Language :: Python :: 3.4', + 'Programming Language :: Python :: 3.5', + 'Programming Language :: Python :: 3.6', 'Topic :: Internet :: WWW/HTTP', ] ) diff --git a/tests/test_codecs.py b/tests/test_codecs.py index eb77a68..32458cd 100644 --- a/tests/test_codecs.py +++ b/tests/test_codecs.py @@ -1,9 +1,10 @@ # coding: utf-8 from coreapi.codecs import CoreJSONCodec -from coreapi.codecs.corejson import _document_to_primative, _primative_to_document +from coreapi.codecs.corejson import _document_to_primitive, _primitive_to_document from coreapi.document import Document, Link, Error, Field from coreapi.exceptions import ParseError, NoCodecAvailable from coreapi.utils import negotiate_decoder, negotiate_encoder +from coreschema import Enum, String import pytest @@ -21,16 +22,22 @@ def doc(): 'integer': 123, 'dict': {'key': 'value'}, 'list': [1, 2, 3], - 'link': Link(url='http://example.org/', fields=[Field(name='example')]), + 'link': Link( + url='http://example.org/', + fields=[ + Field(name='noschema'), + Field(name='string_example', schema=String()), + Field(name='enum_example', schema=Enum(['a', 'b', 'c'])), + ]), 'nested': {'child': Link(url='http://example.org/123')}, '_type': 'needs escaping' }) -# Documents have a mapping to python primatives in JSON style. +# Documents have a mapping to python primitives in JSON style. -def test_document_to_primative(doc): - data = _document_to_primative(doc) +def test_document_to_primitive(doc): + data = _document_to_primitive(doc) assert data == { '_type': 'document', '_meta': { @@ -40,13 +47,32 @@ def test_document_to_primative(doc): 'integer': 123, 'dict': {'key': 'value'}, 'list': [1, 2, 3], - 'link': {'_type': 'link', 'fields': [{'name': 'example'}]}, + 'link': {'_type': 'link', 'fields': [ + {'name': 'noschema'}, + { + 'name': 'string_example', + 'schema': { + '_type': 'string', + 'title': '', + 'description': '', + }, + }, + { + 'name': 'enum_example', + 'schema': { + '_type': 'enum', + 'title': '', + 'description': '', + 'enum': ['a', 'b', 'c'], + }, + }, + ]}, 'nested': {'child': {'_type': 'link', 'url': '/123'}}, '__type': 'needs escaping' } -def test_primative_to_document(doc): +def test_primitive_to_document(doc): data = { '_type': 'document', '_meta': { @@ -56,31 +82,54 @@ def test_primative_to_document(doc): 'integer': 123, 'dict': {'key': 'value'}, 'list': [1, 2, 3], - 'link': {'_type': 'link', 'url': 'http://example.org/', 'fields': [{'name': 'example'}]}, + 'link': { + '_type': 'link', + 'url': 'http://example.org/', + 'fields': [ + {'name': 'noschema'}, + { + 'name': 'string_example', + 'schema': { + '_type': 'string', + 'title': '', + 'description': '', + }, + }, + { + 'name': 'enum_example', + 'schema': { + '_type': 'enum', + 'title': '', + 'description': '', + 'enum': ['a', 'b', 'c'], + }, + }, + ], + }, 'nested': {'child': {'_type': 'link', 'url': 'http://example.org/123'}}, '__type': 'needs escaping' } - assert _primative_to_document(data) == doc + assert _primitive_to_document(data) == doc -def test_error_to_primative(): +def test_error_to_primitive(): error = Error(title='Failure', content={'messages': ['failed']}) data = { '_type': 'error', '_meta': {'title': 'Failure'}, 'messages': ['failed'] } - assert _document_to_primative(error) == data + assert _document_to_primitive(error) == data -def test_primative_to_error(): +def test_primitive_to_error(): error = Error(title='Failure', content={'messages': ['failed']}) data = { '_type': 'error', '_meta': {'title': 'Failure'}, 'messages': ['failed'] } - assert _primative_to_document(data) == error + assert _primitive_to_document(data) == error # Codecs can load a document successfully. diff --git a/tests/test_document.py b/tests/test_document.py index 289a83d..6b06de7 100644 --- a/tests/test_document.py +++ b/tests/test_document.py @@ -50,7 +50,7 @@ def error(): def _dedent(string): """ - Convience function for dedenting multiline strings, + Convenience function for dedenting multiline strings, for string comparison purposes. """ lines = string.splitlines() @@ -129,7 +129,7 @@ def test_error_does_not_support_property_assignment(): error.integer = 456 -# Children in documents are immutable primatives. +# Children in documents are immutable primitives. def test_document_dictionaries_coerced_to_objects(doc): assert isinstance(doc['dict'], Object) diff --git a/tests/test_integration.py b/tests/test_integration.py index b38de9a..0a2e602 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -41,7 +41,7 @@ def test_dump(document): def test_get(monkeypatch): - def mockreturn(self, request): + def mockreturn(self, request, *args, **kwargs): return MockResponse(b'{"_type": "document", "example": 123}') monkeypatch.setattr(requests.Session, 'send', mockreturn) @@ -52,7 +52,7 @@ def mockreturn(self, request): def test_follow(monkeypatch, document): - def mockreturn(self, request): + def mockreturn(self, request, *args, **kwargs): return MockResponse(b'{"_type": "document", "example": 123}') monkeypatch.setattr(requests.Session, 'send', mockreturn) @@ -63,7 +63,7 @@ def mockreturn(self, request): def test_reload(monkeypatch): - def mockreturn(self, request): + def mockreturn(self, request, *args, **kwargs): return MockResponse(b'{"_type": "document", "example": 123}') monkeypatch.setattr(requests.Session, 'send', mockreturn) @@ -75,7 +75,7 @@ def mockreturn(self, request): def test_error(monkeypatch, document): - def mockreturn(self, request): + def mockreturn(self, request, *args, **kwargs): return MockResponse(b'{"_type": "error", "message": ["failed"]}') monkeypatch.setattr(requests.Session, 'send', mockreturn) diff --git a/tests/test_transport.py b/tests/test_transport.py index 09cb849..eeeb17a 100644 --- a/tests/test_transport.py +++ b/tests/test_transport.py @@ -47,7 +47,7 @@ def test_missing_hostname(): # Test basic transition types. def test_get(monkeypatch, http): - def mockreturn(self, request): + def mockreturn(self, request, *args, **kwargs): return MockResponse(b'{"_type": "document", "example": 123}') monkeypatch.setattr(requests.Session, 'send', mockreturn) @@ -58,7 +58,7 @@ def mockreturn(self, request): def test_get_with_parameters(monkeypatch, http): - def mockreturn(self, request): + def mockreturn(self, request, *args, **kwargs): insert = request.path_url.encode('utf-8') return MockResponse( b'{"_type": "document", "url": "' + insert + b'"}' @@ -72,7 +72,7 @@ def mockreturn(self, request): def test_get_with_path_parameter(monkeypatch, http): - def mockreturn(self, request): + def mockreturn(self, request, *args, **kwargs): insert = request.url.encode('utf-8') return MockResponse( b'{"_type": "document", "example": "' + insert + b'"}' @@ -90,7 +90,7 @@ def mockreturn(self, request): def test_post(monkeypatch, http): - def mockreturn(self, request): + def mockreturn(self, request, *args, **kwargs): codec = CoreJSONCodec() body = force_text(request.body) content = codec.encode(Document(content={'data': json.loads(body)})) @@ -104,7 +104,7 @@ def mockreturn(self, request): def test_delete(monkeypatch, http): - def mockreturn(self, request): + def mockreturn(self, request, *args, **kwargs): return MockResponse(b'') monkeypatch.setattr(requests.Session, 'send', mockreturn)