Skip to content

Commit

Permalink
Add @returns.json support for casting response to built-in type
Browse files Browse the repository at this point in the history
Fixes #215
  • Loading branch information
prkumar committed Jan 8, 2022
1 parent 7eebaf1 commit 118141f
Show file tree
Hide file tree
Showing 3 changed files with 137 additions and 15 deletions.
26 changes: 26 additions & 0 deletions tests/integration/test_returns.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,11 @@ def get_repo(self, user, repo):
def get_repos(self, user):
pass

@uplink.returns.from_json(key=("data", 0, "size"))
@uplink.get("/users/{user}/repos")
def get_first_repo_size(self, user):
pass

@uplink.json
@uplink.post("/users/{user}/repos", args={"repo": uplink.Body(Repo)})
def create_repo(self, user, repo):
Expand Down Expand Up @@ -114,6 +119,27 @@ def test_returns_json_with_list(mock_client, mock_response):
] == repo


def test_returns_json_by_key(mock_client, mock_response):
# Setup
mock_response.with_json(
{
"data": [
{"owner": "prkumar", "name": "uplink", "size": 300},
{"owner": "prkumar", "name": "uplink-protobuf", "size": 400},
],
"errors": [],
}
)
mock_client.with_response(mock_response)
github = GitHub(base_url=BASE_URL, client=mock_client)

# Run
size = github.get_first_repo_size("prkumar")

# Verify
assert size == 300


def test_post_json(mock_client):
# Setup
github = GitHub(
Expand Down
67 changes: 64 additions & 3 deletions tests/unit/test_returns.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
# Local imports
import pytest

from uplink import returns


Expand Down Expand Up @@ -59,14 +61,73 @@ def test_returns_json(request_builder, mocker):
mock_response.json()
)

# Verify: Doesn't apply to unsupported types
# Verify: Returns JSON when type cannot be converted
request_builder.get_converter.return_value = None
returns_json = returns.json(str, ())
returns_json = returns.json(None, ())
request_builder.return_type = returns.ReturnType.with_decorator(
None, returns_json
)
returns_json.modify_request(request_builder)
assert callable(request_builder.return_type)
assert request_builder.return_type(mock_response) == mock_response.json()


def test_returns_json_builtin_type(request_builder, mocker):
mock_response = mocker.Mock()
mock_response.json.return_value = {"key": "1"}
request_builder.get_converter.return_value = None
returns_json = returns.json(type=int, key="key")
request_builder.return_type = returns.ReturnType.with_decorator(
None, returns_json
)
returns_json.modify_request(request_builder)
assert not callable(request_builder.return_type)
print(request_builder.return_type)
assert callable(request_builder.return_type)
assert request_builder.return_type(mock_response) == 1


class TestReturnsJsonCast(object):
default_value = {"key": "1"}

@staticmethod
def prepare_test(request_builder, mocker, value=default_value, **kwargs):
mock_response = mocker.Mock()
mock_response.json.return_value = value
request_builder.get_converter.return_value = None
returns_json = returns.json(**kwargs)
request_builder.return_type = returns.ReturnType.with_decorator(
None, returns_json
)
returns_json.modify_request(request_builder)
return mock_response

def test_without_cast(self, request_builder, mocker):
mock_response = self.prepare_test(
request_builder, mocker, type=int, key="key", cast=False
)
assert request_builder.return_type(mock_response) == "1"

def test_with_cast(self, request_builder, mocker):
mock_response = self.prepare_test(
request_builder, mocker, type=lambda _: "test", cast=True
)
assert request_builder.return_type(mock_response) == "test"

def test_with_builtin_type(self, request_builder, mocker):
mock_response = self.prepare_test(request_builder, mocker, type=str)
assert request_builder.return_type(mock_response) == str(
self.default_value
)

def test_with_builtin_type_and_key(self, request_builder, mocker):
mock_response = self.prepare_test(
request_builder, mocker, key="key", type=int
)
assert request_builder.return_type(mock_response) == 1

def test_with_not_callable_cast(self, request_builder, mocker):
with pytest.raises(ValueError):
self.prepare_test(request_builder, mocker, type=1, cast=True)


def test_returns_JsonStrategy(mocker):
Expand Down
59 changes: 47 additions & 12 deletions uplink/returns.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@

__all__ = ["json", "from_json", "schema"]

from uplink.utils import is_subclass


class ReturnType(object):
def __init__(self, decorator, type_):
Expand Down Expand Up @@ -44,9 +46,6 @@ class _ReturnsBase(decorators.MethodAnnotation):
def return_type(self): # pragma: no cover
raise NotImplementedError

def _get_return_type(self, return_type): # pragma: no cover
return return_type

def _make_strategy(self, converter): # pragma: no cover
pass

Expand All @@ -56,15 +55,17 @@ def _modify_request_definition(self, definition, kwargs):
definition.return_type, self
)

def _get_converter(self, request_builder, return_type): # pragma: no cover
return request_builder.get_converter(
keys.CONVERT_FROM_RESPONSE_BODY, return_type.type
)

def modify_request(self, request_builder):
return_type = request_builder.return_type
if not return_type.is_applicable(self):
return

converter = request_builder.get_converter(
keys.CONVERT_FROM_RESPONSE_BODY,
self._get_return_type(return_type.type),
)
converter = self._get_converter(request_builder, return_type)
if converter is not None:
# Found a converter that can handle the return type.
request_builder.return_type = return_type.with_strategy(
Expand Down Expand Up @@ -139,15 +140,23 @@ def get_user(self, username):
.. versionadded:: v0.5.0
"""

_builtin_types = (dict, list, str, int, float)
_can_be_static = True

class _DummyConverter(interfaces.Converter):
def convert(self, response):
return response

class _CastConverter(interfaces.Converter):
def __init__(self, cast):
self._cast = cast

def convert(self, response):
return self._cast(response)

__dummy_converter = _DummyConverter()

def __init__(self, type=None, key=(), model=None, member=()):
def __init__(self, type=None, key=(), cast=None, model=None, member=()):
if model: # pragma: no cover
warnings.warn(
"The `model` argument of @returns.json is deprecated and will "
Expand All @@ -162,19 +171,45 @@ def __init__(self, type=None, key=(), model=None, member=()):
)
self._type = type or model
self._key = key or member
self._cast = cast

if self._cast and not callable(self._type):
raise ValueError(
"When the `cast` argument is True, the `type` argument is "
"expected to be callable."
)

@property
def return_type(self):
return self._type

def _get_return_type(self, return_type):
# If return_type is None, the strategy should directly return
# the JSON body of the HTTP response, instead of trying to
def _get_converter(self, request_builder, return_type):
converter = super(json, self)._get_converter(
request_builder, return_type
)

if converter:
return converter

cast = self._get_cast(return_type)
if cast:
return self._CastConverter(cast)

# If the return_type cannot be converted, the strategy should directly
# return the JSON body of the HTTP response, instead of trying to
# deserialize it into a certain type. In this case, by
# defaulting the return type to the dummy converter, which
# implements this pass-through behavior, we ensure that
# _make_strategy is called.
return self.__dummy_converter if return_type is None else return_type
return self.__dummy_converter

def _get_cast(self, return_type):
if self._cast:
return return_type.type
if self._cast is None and is_subclass(
return_type.type, self._builtin_types
):
return return_type.type

def _make_strategy(self, converter):
return JsonStrategy(converter, self._key)
Expand Down

0 comments on commit 118141f

Please sign in to comment.