Skip to content

Commit

Permalink
Merge pull request #214 from prkumar/master
Browse files Browse the repository at this point in the history
Release v0.9.4
  • Loading branch information
prkumar committed Feb 15, 2021
2 parents e84e10c + 3ade04d commit 1649e43
Show file tree
Hide file tree
Showing 14 changed files with 105 additions and 71 deletions.
21 changes: 17 additions & 4 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,29 @@ All notable changes to this project will be documented in this file.
The format is based on `Keep a Changelog`_, and this project adheres to the
`Semantic Versioning`_ scheme.

0.9.4_ - 2021-02-15
====================
Fixed
-----
- A type set as a consumer method's return annotation should not be used to
deserialize a response object if no registered converters can handle the type.
(`3653a672ee`_)

0.9.3_ - 2020-11-22
====================
Added
-----
- Support for serialization using a subclass of `pydantic`_'s `BaseModel` that
contains fields of a complex type, such as `datetime`.
- Support for serialization using a subclass of `pydantic`_'s ``BaseModel`` that
contains fields of a complex type, such as ``datetime``.
(`#207`_ by `@leiserfg`_)
- Support for passing a subclass of `pydantic`'s `BaseModel` as the request
- Support for passing a subclass of `pydantic`'s ``BaseModel`` as the request
body. (`#209`_ by `@lust4life`_)

0.9.2_ - 2020-10-18
====================
Added
-----
- Support for (de)serializing subclasses of `pydantic`_'s `BaseModel`
- Support for (de)serializing subclasses of `pydantic`_'s ``BaseModel``
(`#200`_ by `@gmcrocetti`_)

Fixed
Expand Down Expand Up @@ -333,6 +341,8 @@ Added
.. _pydantic: https://pydantic-docs.helpmanual.io/

.. Releases
.. _0.9.4: https://github.com/prkumar/uplink/compare/v0.9.3...v0.9.4
.. _0.9.3: https://github.com/prkumar/uplink/compare/v0.9.2...v0.9.3
.. _0.9.2: https://github.com/prkumar/uplink/compare/v0.9.1...v0.9.2
.. _0.9.1: https://github.com/prkumar/uplink/compare/v0.9.0...v0.9.1
.. _0.9.0: https://github.com/prkumar/uplink/compare/v0.8.0...v0.9.0
Expand Down Expand Up @@ -380,6 +390,9 @@ Added
.. _#207: https://github.com/prkumar/uplink/pull/207
.. _#209: https://github.com/prkumar/uplink/pull/209

.. Commits
.. _3653a672ee: https://github.com/prkumar/uplink/commit/3653a672ee0703119720d0077bb450649af5459c

.. Contributors
.. _@daa: https://github.com/daa
.. _@SakornW: https://github.com/SakornW
Expand Down
75 changes: 51 additions & 24 deletions tests/unit/test_converters.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,40 +18,66 @@
from uplink.converters import register, standard


class TestCast(object):
def test_converter_without_caster(self, mocker):
converter_mock = mocker.stub()
converter_mock.return_value = 2
cast = standard.Cast(None, converter_mock)
return_value = cast.convert(1)
converter_mock.assert_called_with(1)
assert return_value == 2

def test_convert_with_caster(self, mocker):
caster = mocker.Mock(return_value=2)
converter_mock = mocker.Mock(return_value=3)
cast = standard.Cast(caster, converter_mock)
return_value = cast.convert(1)
caster.assert_called_with(1)
converter_mock.assert_called_with(2)
assert return_value == 3


class TestStringConverter(object):
def test_convert(self):
converter_ = standard.StringConverter()
assert converter_.convert(2) == "2"


class TestStandardConverter(object):
def test_create_response_body_converter(self, converter_mock):
def test_create_response_body_converter_with_converter(
self, converter_mock
):
# Setup
factory = standard.StandardConverter()

# Run & Verify: Pass-through converters
converter = factory.create_response_body_converter(converter_mock)
assert converter is converter_mock

def test_create_response_body_converter_with_unknown_type(self):
# Setup
factory = standard.StandardConverter()

# Run & Verify: does not know how to create converter when given a type
# that is not a converter
converter = factory.create_response_body_converter(dict)
assert converter is None

def test_create_request_body_converter_with_converter(self, converter_mock):
# Setup
factory = standard.StandardConverter()

# Run & Verify: Pass-through converters
converter = factory.create_request_body_converter(converter_mock)
assert converter is converter_mock

def test_create_request_body_converter_with_unknown_type(self):
# Setup
factory = standard.StandardConverter()

# Run & Verify: does not know how to create converter when given a type
# that is not a converter
converter = factory.create_response_body_converter(dict)
assert converter is None

def test_create_string_converter_with_converter(self, converter_mock):
# Setup
factory = standard.StandardConverter()

# Run & Verify: Pass-through converters
converter = factory.create_string_converter(converter_mock)
assert converter is converter_mock

def test_create_string_converter_with_unknown_type(self):
# Setup
factory = standard.StandardConverter()

# Run & Verify: creates string converter when given type is not a
# converter
converter = factory.create_string_converter(dict)
assert isinstance(converter, standard.StringConverter)


class TestConverterFactoryRegistry(object):
backend = converters.ConverterFactoryRegistry._converter_factory_registry
Expand Down Expand Up @@ -487,19 +513,20 @@ def test_create_request_body_converter(self, pydantic_model_mock):
def test_convert_complex_model(self):
from json import loads
from datetime import datetime
from typing import List

class ComplexModel(pydantic.BaseModel):
when = datetime.utcnow() # type: datetime
where = 'http://example.com' # type: pydantic.AnyUrl
some = [1] # type: List[int]
where = "http://example.com" # type: pydantic.AnyUrl
some = [1] # type: typing.List[int]

model = ComplexModel()
request_body = {}
expected_result = loads(model.json())

converter = converters.PydanticConverter()
request_converter = converter.create_request_body_converter(ComplexModel)
request_converter = converter.create_request_body_converter(
ComplexModel
)

result = request_converter.convert(request_body)

Expand Down
2 changes: 1 addition & 1 deletion tests/unit/test_decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -260,7 +260,7 @@ def test_json_list(request_builder):
def test_timeout(request_builder):
timeout = decorators.timeout(60)
timeout.modify_request(request_builder)
request_builder.info["timeout"] == 60
assert request_builder.info["timeout"] == 60


def test_args(request_definition_builder):
Expand Down
4 changes: 2 additions & 2 deletions tests/unit/test_hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,8 @@ def test_delegate_handle_response_multiple(self, mocker):
hooks.ResponseHandler(mock_response_handler),
)
chain.handle_response("consumer", {})
mock_response_handler.call_count == 2
mock_request_auditor.call_count == 1
assert mock_response_handler.call_count == 2
assert mock_request_auditor.call_count == 0

def test_delegate_handle_exception(self, transaction_hook_mock):
class CustomException(Exception):
Expand Down
2 changes: 1 addition & 1 deletion uplink/__about__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@
that is used both in distribution (i.e., setup.py) and within the
codebase.
"""
__version__ = "0.9.3"
__version__ = "0.9.4"
7 changes: 4 additions & 3 deletions uplink/arguments.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"""
# Standard library imports
import collections
from collections import abc
import functools
import inspect

Expand Down Expand Up @@ -74,7 +75,7 @@ def remaining_args_count(self):

def set_annotations(self, annotations=None, **more_annotations):
if annotations is not None:
if not isinstance(annotations, collections.Mapping):
if not isinstance(annotations, abc.Mapping):
missing = tuple(
a
for a in self.missing_arguments
Expand Down Expand Up @@ -392,7 +393,7 @@ def _update_params(info, existing, new_params, encoded):
@staticmethod
def update_params(info, new_params, encoded):
existing = info.setdefault("params", None if encoded else dict())
if encoded == isinstance(existing, collections.Mapping):
if encoded == isinstance(existing, abc.Mapping):
raise Query.QueryStringEncodingError()
Query._update_params(info, existing, new_params, encoded)

Expand Down Expand Up @@ -802,7 +803,7 @@ def converter_key(self):

def _modify_request(self, request_builder, value):
"""Updates the context with the given name-value pairs."""
if not isinstance(value, collections.Mapping):
if not isinstance(value, abc.Mapping):
raise TypeError(
"ContextMap requires a mapping; got %s instead.", type(value)
)
Expand Down
4 changes: 2 additions & 2 deletions uplink/auth.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""This module implements the auth layer."""

# Standard library imports
import collections
from collections import abc

# Third-party imports
from requests import auth
Expand All @@ -22,7 +22,7 @@
def get_auth(auth_object=None):
if auth_object is None:
return utils.no_op
elif isinstance(auth_object, collections.Iterable):
elif isinstance(auth_object, abc.Iterable):
return BasicAuth(*auth_object)
elif callable(auth_object):
return auth_object
Expand Down
4 changes: 2 additions & 2 deletions uplink/clients/io/interfaces.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Standard library imports
import collections
from collections import abc

# Local imports
from uplink import compat
Expand Down Expand Up @@ -69,7 +69,7 @@ def on_failure(self, exc_type, exc_val, exc_tb):
raise NotImplementedError


class Executable(collections.Iterator):
class Executable(abc.Iterator):
"""An abstraction for iterating over the execution of a request."""

def __next__(self):
Expand Down
4 changes: 4 additions & 0 deletions uplink/clients/io/transitions.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ def fail(exc_type, exc_val, exc_tb):
"""
Transitions the execution to fail with a specific error.
This will prompt the execution of any RequestTemplate.after_exception hooks.
Args:
exc_type: The exception class.
exc_val: The exception object.
Expand All @@ -63,6 +65,8 @@ def prepare(request):
"""
Transitions the execution to prepare the given request.
This will prompt the execution of any RequestTemplate.before_request.
Args:
request: The intended request data to be sent.
"""
Expand Down
4 changes: 2 additions & 2 deletions uplink/commands.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Standard library imports
import collections
from collections import abc
import functools

# Local imports
Expand Down Expand Up @@ -321,7 +321,7 @@ def __init__(self, method, uri=None, args=None):

# Register argument annotations
if args:
is_map = isinstance(args, collections.Mapping)
is_map = isinstance(args, abc.Mapping)
args, kwargs = ((), args) if is_map else (args, {})
self._add_args = decorators.args(*args, **kwargs)

Expand Down
4 changes: 2 additions & 2 deletions uplink/converters/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Standard library imports
import collections
from collections import abc

# Local imports
from uplink._extras import installer, plugin
Expand Down Expand Up @@ -57,7 +57,7 @@ def __call__(self, *args, **kwargs):
return converter


class ConverterFactoryRegistry(collections.Mapping):
class ConverterFactoryRegistry(abc.Mapping):
"""
A registry that chains together
:py:class:`interfaces.ConverterFactory` instances.
Expand Down
32 changes: 10 additions & 22 deletions uplink/converters/standard.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,6 @@
from uplink.converters import interfaces, register_default_converter_factory


class Cast(interfaces.Converter):
def __init__(self, caster, converter):
self._cast = caster
self._converter = converter

def set_chain(self, chain):
self._converter.set_chain(chain)

def convert(self, value):
if callable(self._cast):
value = self._cast(value)
return self._converter(value)


class StringConverter(interfaces.Converter):
def convert(self, value):
return str(value)
Expand All @@ -29,13 +15,15 @@ class StandardConverter(interfaces.Factory):
converters could handle a particular type.
"""

@staticmethod
def pass_through_converter(type_, *args, **kwargs):
return type_
def create_request_body_converter(self, cls, *args, **kwargs):
if isinstance(cls, interfaces.Converter):
return cls

create_response_body_converter = (
create_request_body_converter
) = pass_through_converter
def create_response_body_converter(self, cls, *args, **kwargs):
if isinstance(cls, interfaces.Converter):
return cls

def create_string_converter(self, type_, *args, **kwargs):
return Cast(type_, StringConverter()) # pragma: no cover
def create_string_converter(self, cls, *args, **kwargs):
if isinstance(cls, interfaces.Converter):
return cls
return StringConverter()
5 changes: 3 additions & 2 deletions uplink/converters/typing_.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# Standard library imports
import collections
from collections import abc
import functools

# Local imports
Expand All @@ -25,7 +26,7 @@ def set_chain(self, chain):
self._elem_converter = chain(self._elem_type) or self._elem_type

def convert(self, value):
if isinstance(value, collections.Sequence):
if isinstance(value, abc.Sequence):
return list(map(self._elem_converter, value))
else:
# TODO: Handle the case where the value is not an sequence.
Expand All @@ -44,7 +45,7 @@ def set_chain(self, chain):
self._value_converter = chain(self._value_type) or self._value_type

def convert(self, value):
if isinstance(value, collections.Mapping):
if isinstance(value, abc.Mapping):
key_c, val_c = self._key_converter, self._value_converter
return dict((key_c(k), val_c(value[k])) for k in value)
else:
Expand Down

0 comments on commit 1649e43

Please sign in to comment.