From 36a9e8e6324e1ac1ed68ef8686fddf98eb189869 Mon Sep 17 00:00:00 2001 From: "P. Raj Kumar" Date: Thu, 6 Jun 2019 18:25:59 -0700 Subject: [PATCH 01/19] Bump minor version to v0.10.0 --- uplink/__about__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/uplink/__about__.py b/uplink/__about__.py index 0d780a21..34dadf35 100644 --- a/uplink/__about__.py +++ b/uplink/__about__.py @@ -3,4 +3,4 @@ that is used both in distribution (i.e., setup.py) and within the codebase. """ -__version__ = "0.9.0" +__version__ = "0.10.0" From 0a684344d2d16b63f9c1d973463b32ad49fe4388 Mon Sep 17 00:00:00 2001 From: "P. Raj Kumar" Date: Sat, 10 Aug 2019 01:34:14 -0700 Subject: [PATCH 02/19] Pin twisted to <=v19.2.1 for py3.4 (#168) Twisted 19.7.0 drops py3.4 support. --- setup.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 661e5b6c..8e2db553 100644 --- a/setup.py +++ b/setup.py @@ -21,8 +21,11 @@ def read(filename): "marshmallow": ["marshmallow>=2.15.0"], "aiohttp:python_version <= '3.4'": [], "aiohttp:python_version >= '3.4'": "aiohttp>=2.3.0", - "twisted:python_version != '3.3'": "twisted>=17.1.0", + "twisted:python_version != '3.3' and python_version != '3.4'": "twisted>=17.1.0", + # Twisted 18.4.0 dropped py3.3 support "twisted:python_version == '3.3'": "twisted<=17.9.0", + # Twisted 19.7.0 dropped py3.4 support + "twisted:python_version == '3.4'": "twisted<=19.2.1", "typing": ["typing>=3.6.4"], "tests": ["pytest<4.1", "pytest-mock", "pytest-cov", "pytest-twisted"], } From 66e3a5aaa5069289b0e038a7cbf8483f86afb31f Mon Sep 17 00:00:00 2001 From: "P. Raj Kumar" Date: Sat, 10 Aug 2019 01:42:10 -0700 Subject: [PATCH 03/19] Omit `Header` with `None` value from request (#169) Fixes #167. --- CHANGELOG.rst | 10 +++++++- tests/unit/test_arguments.py | 4 +++ uplink/arguments.py | 49 +++++++++++++++++++++++++----------- 3 files changed, 47 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index d87df8e9..62d49bc0 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -6,6 +6,13 @@ 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. +# Unreleased_ +============= +Changed +------- +- Omit ``Header`` argument from request when its value is ``None``. + (`#167`_, `#169`_) + 0.9.0_ - 2019-06-05 =================== Added @@ -29,7 +36,7 @@ Fixed Changed ------- -- Renamed ``uplink.retry.stop.DISABLE`` to ``uplink.retry.stop.NEVER`` +- Rename ``uplink.retry.stop.DISABLE`` to ``uplink.retry.stop.NEVER`` 0.8.0_ - 2019-02-16 =================== @@ -294,6 +301,7 @@ Added .. _`Semantic Versioning`: https://packaging.python.org/tutorials/distributing-packages/#semantic-versioning-preferred .. Releases +.. _Unreleased: https://github.com/prkumar/uplink/compare/v0.9.0...HEAD .. _0.9.0: https://github.com/prkumar/uplink/compare/v0.8.0...v0.9.0 .. _0.8.0: https://github.com/prkumar/uplink/compare/v0.7.0...v0.8.0 .. _0.7.0: https://github.com/prkumar/uplink/compare/v0.6.1...v0.7.0 diff --git a/tests/unit/test_arguments.py b/tests/unit/test_arguments.py index c8e9be00..73581c35 100644 --- a/tests/unit/test_arguments.py +++ b/tests/unit/test_arguments.py @@ -327,6 +327,10 @@ def test_modify_request(self, request_builder): arguments.Header("hello").modify_request(request_builder, "world") assert request_builder.info["headers"] == {"hello": "world"} + def test_skip_none(self, request_builder): + arguments.Header("hello").modify_request(request_builder, None) + assert request_builder.info["headers"] == {} + class TestHeaderMap(ArgumentTestCase, FuncDecoratorTestCase): type_cls = arguments.HeaderMap diff --git a/uplink/arguments.py b/uplink/arguments.py index a888c10e..e4297801 100644 --- a/uplink/arguments.py +++ b/uplink/arguments.py @@ -225,6 +225,23 @@ def converter_key(self): # pragma: no cover raise NotImplementedError +class EncodeNoneMixin(object): + #: Identifies how a `None` value should be encoded in the request. + _encode_none = None # type: str + + def _modify_request(self, request_builder, value): # pragma: no cover + raise NotImplementedError + + def modify_request(self, request_builder, value): + if value is None: + if self._encode_none is None: + # ignore value if it is None and shouldn't be encoded + return + else: + value = self._encode_none + super(EncodeNoneMixin, self).modify_request(request_builder, value) + + class FuncDecoratorMixin(object): @classmethod def _is_static_call(cls, *args_, **kwargs): @@ -304,7 +321,7 @@ def _modify_request(self, request_builder, value): request_builder.url.set_variable({self.name: value}) -class Query(FuncDecoratorMixin, NamedArgument): +class Query(FuncDecoratorMixin, EncodeNoneMixin, NamedArgument): """ Set a dynamic query parameter. @@ -392,14 +409,6 @@ def _modify_request(self, request_builder, value): request_builder.info, {self.name: value}, self._encoded ) - def modify_request(self, request_builder, value): - if value is None: - # ignore value if it is None and shouldn't be encoded - if self._encode_none is not None: - self._modify_request(request_builder, self._encode_none) - else: - super(Query, self).modify_request(request_builder, value) - class QueryMap(FuncDecoratorMixin, TypedArgument): """ @@ -438,20 +447,30 @@ def _modify_request(self, request_builder, value): Query.update_params(request_builder.info, value, self._encoded) -class Header(FuncDecoratorMixin, NamedArgument): +class Header(FuncDecoratorMixin, EncodeNoneMixin, NamedArgument): """ Pass a header as a method argument at runtime. - While :py:class:`uplink.headers` attaches static headers - that define all requests sent from a consumer method, this - class turns a method argument into a dynamic header value. + While :py:class:`uplink.headers` attaches request headers values + that are static across all requests sent from the decorated + consumer method, this annotation turns a method argument into a + dynamic request header. Example: .. code-block:: python @get("/user") - def (self, session_id: Header("Authorization")): - \"""Get the authenticated user\""" + def me(self, session_id: Header("Authorization")): + \"""Get the authenticated user.\""" + + To define an optional header, use the default value of `None`: + + @get("/repositories") + def fetch_repos(self, auth: Header("Authorization") = None): + \"""List all public repositories.\""" + + When the argument is not used, the header will not be added + to the request. """ @property From d422255ca114c0e48f420b6cc7974b3b4b2970c2 Mon Sep 17 00:00:00 2001 From: "P. Raj Kumar" Date: Sat, 10 Aug 2019 01:55:48 -0700 Subject: [PATCH 04/19] Clarify section titles in quickstart --- docs/source/user/quickstart.rst | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/docs/source/user/quickstart.rst b/docs/source/user/quickstart.rst index 9c30c1cb..f2ca05c0 100644 --- a/docs/source/user/quickstart.rst +++ b/docs/source/user/quickstart.rst @@ -116,8 +116,8 @@ Requests for a different backing HTTP client, such as :ref:`aiohttp `__ that depend on method @@ -139,8 +139,11 @@ or use the :py:class:`~uplink.Path` annotation. @get("users/{username}") def get_user(self, name: Path("username")): pass -:py:class:`~uplink.Query` parameters can also be added dynamically -by method arguments. +Query Parameters +================ + +Query parameters can be added dynamically using the :py:class:`~uplink.Query` +argument annotation. .. code-block:: python @@ -193,8 +196,8 @@ parameters that need to be included with every request: class GitHub(Consumer): ... -Header Manipulation -=================== +Request Headers +=============== You can set static headers for a method using the :py:class:`@headers ` decorator. From 3d776c7a4f5036ee433ec86a809f05af59eb00d8 Mon Sep 17 00:00:00 2001 From: "P. Raj Kumar" Date: Sat, 10 Aug 2019 17:00:15 -0700 Subject: [PATCH 05/19] Update CHANGELOG.rst --- CHANGELOG.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 62d49bc0..9e8fc108 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -6,8 +6,8 @@ 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. -# Unreleased_ -============= +Unreleased_ +=========== Changed ------- - Omit ``Header`` argument from request when its value is ``None``. From 801ff4fe95d40602e3c45a84a8ef90e22a2085ce Mon Sep 17 00:00:00 2001 From: "P. Raj Kumar" Date: Sat, 10 Aug 2019 17:02:17 -0700 Subject: [PATCH 06/19] Update CHANGELOG.rst --- CHANGELOG.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 9e8fc108..5e2c66a6 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -334,6 +334,8 @@ Added .. _#154: https://github.com/prkumar/uplink/pull/154 .. _#155: https://github.com/prkumar/uplink/pull/155 .. _#159: https://github.com/prkumar/uplink/pull/159 +.. _#167: https://github.com/prkumar/uplink/issues/167 +.. _#169: https://github.com/prkumar/uplink/pull/169 .. Contributors .. _@daa: https://github.com/daa From 6b027973ec4bfc067239787f3f92f0fb59041287 Mon Sep 17 00:00:00 2001 From: Jacob Floyd Date: Sat, 15 Jun 2019 21:18:00 -0500 Subject: [PATCH 07/19] Fix uplink.Url by checking url type (fixes #164) --- uplink/commands.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/uplink/commands.py b/uplink/commands.py index d1033487..bf9e310a 100644 --- a/uplink/commands.py +++ b/uplink/commands.py @@ -268,7 +268,8 @@ def define_request(self, request_builder, func_args, func_kwargs): request_builder, func_args, func_kwargs ) self._method_handler.handle_builder(request_builder) - request_builder.url = request_builder.url.build() + if isinstance(request_builder.url, utils.URIBuilder): + request_builder.url = request_builder.url.build() class HttpMethodFactory(object): From 6276522f3a33f772d8747cb69755e41de31520bb Mon Sep 17 00:00:00 2001 From: Jacob Floyd Date: Thu, 20 Jun 2019 22:53:38 -0500 Subject: [PATCH 08/19] Update arguments.Url to use utils.URIBuilder https://github.com/prkumar/uplink/pull/165#issuecomment-504097881 --- uplink/arguments.py | 3 ++- uplink/commands.py | 5 +++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/uplink/arguments.py b/uplink/arguments.py index e4297801..069a8b9a 100644 --- a/uplink/arguments.py +++ b/uplink/arguments.py @@ -658,6 +658,7 @@ def _modify_request(self, request_builder, value): request_builder.info["data"] = value +# TODO: Add an integration test that uses arguments.Url class Url(ArgumentAnnotation): """ Sets a dynamic URL. @@ -698,7 +699,7 @@ def modify_request_definition(self, request_definition_builder): @classmethod def _modify_request(cls, request_builder, value): """Updates request url.""" - request_builder.url = value + request_builder.url = utils.URIBuilder(value) class Timeout(FuncDecoratorMixin, ArgumentAnnotation): diff --git a/uplink/commands.py b/uplink/commands.py index bf9e310a..f7ad8c76 100644 --- a/uplink/commands.py +++ b/uplink/commands.py @@ -268,8 +268,9 @@ def define_request(self, request_builder, func_args, func_kwargs): request_builder, func_args, func_kwargs ) self._method_handler.handle_builder(request_builder) - if isinstance(request_builder.url, utils.URIBuilder): - request_builder.url = request_builder.url.build() + # if an argument or method handler changes request_builder.url + # they MUST use utils.URIBuilder, not a string. + request_builder.url = request_builder.url.build() class HttpMethodFactory(object): From 3e1652558e5bac2cfc8fbfac3c2c01a4c9cc6f57 Mon Sep 17 00:00:00 2001 From: "P. Raj Kumar" Date: Sun, 18 Aug 2019 10:42:42 -0700 Subject: [PATCH 09/19] Build relative URL behind RequestBuilder interface --- tests/integration/test_basic.py | 13 +++++++++++++ tests/unit/test_arguments.py | 4 ++-- tests/unit/test_builder.py | 10 +++++++--- tests/unit/test_commands.py | 2 +- tests/unit/test_helpers.py | 22 ++++++++++++++++++++++ uplink/arguments.py | 4 ++-- uplink/builder.py | 10 ++++++++-- uplink/commands.py | 5 +---- uplink/helpers.py | 17 ++++++++++------- 9 files changed, 66 insertions(+), 21 deletions(-) diff --git a/tests/integration/test_basic.py b/tests/integration/test_basic.py index b63692b6..bc5804c5 100644 --- a/tests/integration/test_basic.py +++ b/tests/integration/test_basic.py @@ -21,6 +21,10 @@ def list_repos(self, user): def get_repo(self, user, repo): pass + @uplink.get(args={"url": uplink.Url}) + def forward(self, url): + pass + def test_list_repo(mock_client): github = GitHubService(base_url=BASE_URL, client=mock_client) @@ -51,6 +55,15 @@ def test_get_repo(mock_client, mock_response): assert expected_json == actual_json +def test_forward(mock_client): + github = GitHubService(base_url=BASE_URL, client=mock_client) + github.forward("/users/prkumar/repos") + request = mock_client.history[0] + assert request.method == "GET" + assert request.has_base_url(BASE_URL) + assert request.has_endpoint("/users/prkumar/repos") + + def test_handle_client_exceptions(mock_client): # Setup: mock client exceptions diff --git a/tests/unit/test_arguments.py b/tests/unit/test_arguments.py index 73581c35..3d06ce40 100644 --- a/tests/unit/test_arguments.py +++ b/tests/unit/test_arguments.py @@ -249,7 +249,7 @@ def test_modify_request_definition(self, request_definition_builder): def test_modify_request(self, request_builder): arguments.Path("name").modify_request(request_builder, "value") - request_builder.url.set_variable.assert_called_with({"name": "value"}) + request_builder.set_url_variable.assert_called_with({"name": "value"}) class TestQuery(ArgumentTestCase, FuncDecoratorTestCase): @@ -425,7 +425,7 @@ def test_modify_request_definition_failure( def test_modify_request(self, request_builder): arguments.Url().modify_request(request_builder, "/some/path") - assert request_builder.url == "/some/path" + assert request_builder.relative_url == "/some/path" class TestTimeout(ArgumentTestCase, FuncDecoratorTestCase): diff --git a/tests/unit/test_builder.py b/tests/unit/test_builder.py index d4652f11..333e70db 100644 --- a/tests/unit/test_builder.py +++ b/tests/unit/test_builder.py @@ -25,7 +25,7 @@ def uplink_builder(http_client_mock): class TestRequestPreparer(object): def test_prepare_request(self, mocker, request_builder): request_builder.method = "METHOD" - request_builder.url = "/example/path" + request_builder.relative_url = "/example/path" request_builder.return_type = None request_builder.transaction_hooks = () request_builder.request_template = "request_template" @@ -45,7 +45,7 @@ def test_prepare_request_with_transaction_hook( self, mocker, uplink_builder, request_builder, transaction_hook_mock ): request_builder.method = "METHOD" - request_builder.url = "/example/path" + request_builder.relative_url = "/example/path" request_builder.request_template = "request_template" uplink_builder.base_url = "https://example.com" request_builder.transaction_hooks = [transaction_hook_mock] @@ -105,7 +105,11 @@ def test_call(self, mocker, request_definition, request_builder): request_builder, execution_builder ) execution_builder.build().start.assert_called_with( - (request_builder.method, request_builder.url, request_builder.info) + ( + request_builder.method, + request_builder.relative_url, + request_builder.info, + ) ) diff --git a/tests/unit/test_commands.py b/tests/unit/test_commands.py index ea44c363..1fbcfa99 100644 --- a/tests/unit/test_commands.py +++ b/tests/unit/test_commands.py @@ -228,7 +228,7 @@ def test_define_request(self, request_builder, mocker): ) definition.define_request(request_builder, (), {}) assert request_builder.method == method - assert request_builder.url == uri + assert request_builder.relative_url == uri assert request_builder.return_type is str def test_make_converter_registry(self, annotation_handler_mock): diff --git a/tests/unit/test_helpers.py b/tests/unit/test_helpers.py index 7f8f7fe9..a3447868 100644 --- a/tests/unit/test_helpers.py +++ b/tests/unit/test_helpers.py @@ -1,3 +1,6 @@ +import pytest + +# Local imports from uplink import helpers @@ -53,3 +56,22 @@ def test_context(self): # Verify assert builder.context["key"] == "value" + + def test_relative_url_template(self): + # Setup + builder = helpers.RequestBuilder(None, {}, "base_url") + + # Run + builder.relative_url = "/v1/api/users/{username}/repos" + builder.set_url_variable({"username": "cognifloyd"}) + + # Verify + assert builder.relative_url == "/v1/api/users/cognifloyd/repos" + + def test_relative_url_template_type_error(self): + # Setup + builder = helpers.RequestBuilder(None, {}, "base_url") + + # Run + with pytest.raises(TypeError): + builder.relative_url = 1 diff --git a/uplink/arguments.py b/uplink/arguments.py index 069a8b9a..76dc146f 100644 --- a/uplink/arguments.py +++ b/uplink/arguments.py @@ -318,7 +318,7 @@ def modify_request_definition(self, request_definition_builder): request_definition_builder.uri.add_variable(self.name) def _modify_request(self, request_builder, value): - request_builder.url.set_variable({self.name: value}) + request_builder.set_url_variable({self.name: value}) class Query(FuncDecoratorMixin, EncodeNoneMixin, NamedArgument): @@ -699,7 +699,7 @@ def modify_request_definition(self, request_definition_builder): @classmethod def _modify_request(cls, request_builder, value): """Updates request url.""" - request_builder.url = utils.URIBuilder(value) + request_builder.relative_url = value class Timeout(FuncDecoratorMixin, ArgumentAnnotation): diff --git a/uplink/builder.py b/uplink/builder.py index 12afdf54..8c988291 100644 --- a/uplink/builder.py +++ b/uplink/builder.py @@ -62,7 +62,9 @@ def apply_hooks(self, execution_builder, chain): execution_builder.with_errbacks(self._wrap_hook(chain.handle_exception)) def prepare_request(self, request_builder, execution_builder): - request_builder.url = self._join_url_with_base(request_builder.url) + request_builder.relative_url = self._join_url_with_base( + request_builder.relative_url + ) self._auth(request_builder) request_hooks = self._get_request_hooks(request_builder) if request_hooks: @@ -104,7 +106,11 @@ def __call__(self, *args, **kwargs): execution = execution_builder.build() return execution.start( # TODO: Create request value object - (request_builder.method, request_builder.url, request_builder.info) + ( + request_builder.method, + request_builder.relative_url, + request_builder.info, + ) ) diff --git a/uplink/commands.py b/uplink/commands.py index f7ad8c76..c97820f5 100644 --- a/uplink/commands.py +++ b/uplink/commands.py @@ -262,15 +262,12 @@ def make_converter_registry(self, converters_): def define_request(self, request_builder, func_args, func_kwargs): request_builder.method = self._method - request_builder.url = utils.URIBuilder(self._uri) + request_builder.relative_url = self._uri request_builder.return_type = self._return_type self._argument_handler.handle_call( request_builder, func_args, func_kwargs ) self._method_handler.handle_builder(request_builder) - # if an argument or method handler changes request_builder.url - # they MUST use utils.URIBuilder, not a string. - request_builder.url = request_builder.url.build() class HttpMethodFactory(object): diff --git a/uplink/helpers.py b/uplink/helpers.py index 82405107..43ed98dc 100644 --- a/uplink/helpers.py +++ b/uplink/helpers.py @@ -2,7 +2,7 @@ import collections # Local imports -from uplink import interfaces +from uplink import interfaces, utils from uplink.clients import io @@ -39,7 +39,7 @@ def set_api_definition(service, name, definition): class RequestBuilder(object): def __init__(self, client, converter_registry, base_url): self._method = None - self._url = None + self._relative_url_template = utils.URIBuilder("") self._return_type = None self._client = client self._base_url = base_url @@ -69,13 +69,16 @@ def method(self, method): def base_url(self): return self._base_url + def set_url_variable(self, variables): + self._relative_url_template.set_variable(variables) + @property - def url(self): - return self._url + def relative_url(self): + return self._relative_url_template.build() - @url.setter - def url(self, url): - self._url = url + @relative_url.setter + def relative_url(self, url): + self._relative_url_template = utils.URIBuilder(url) @property def info(self): From 48d1952671d1930fecf56a91c9b5365a3eff1004 Mon Sep 17 00:00:00 2001 From: "P. Raj Kumar" Date: Sun, 18 Aug 2019 10:51:09 -0700 Subject: [PATCH 10/19] Build full URL in RequestBuilder --- tests/unit/test_builder.py | 6 +----- uplink/builder.py | 12 +----------- uplink/helpers.py | 4 ++++ 3 files changed, 6 insertions(+), 16 deletions(-) diff --git a/tests/unit/test_builder.py b/tests/unit/test_builder.py index 333e70db..93cb0a3e 100644 --- a/tests/unit/test_builder.py +++ b/tests/unit/test_builder.py @@ -105,11 +105,7 @@ def test_call(self, mocker, request_definition, request_builder): request_builder, execution_builder ) execution_builder.build().start.assert_called_with( - ( - request_builder.method, - request_builder.relative_url, - request_builder.info, - ) + (request_builder.method, request_builder.url, request_builder.info) ) diff --git a/uplink/builder.py b/uplink/builder.py index 8c988291..d2f48027 100644 --- a/uplink/builder.py +++ b/uplink/builder.py @@ -33,9 +33,6 @@ def __init__(self, builder, consumer=None): else: self._session_chain = None - def _join_url_with_base(self, url): - return utils.urlparse.urljoin(self._base_url, url) - @staticmethod def _get_request_hooks(contract): chain = list(contract.transaction_hooks) @@ -62,9 +59,6 @@ def apply_hooks(self, execution_builder, chain): execution_builder.with_errbacks(self._wrap_hook(chain.handle_exception)) def prepare_request(self, request_builder, execution_builder): - request_builder.relative_url = self._join_url_with_base( - request_builder.relative_url - ) self._auth(request_builder) request_hooks = self._get_request_hooks(request_builder) if request_hooks: @@ -106,11 +100,7 @@ def __call__(self, *args, **kwargs): execution = execution_builder.build() return execution.start( # TODO: Create request value object - ( - request_builder.method, - request_builder.relative_url, - request_builder.info, - ) + (request_builder.method, request_builder.url, request_builder.info) ) diff --git a/uplink/helpers.py b/uplink/helpers.py index 43ed98dc..d2490b66 100644 --- a/uplink/helpers.py +++ b/uplink/helpers.py @@ -107,6 +107,10 @@ def return_type(self, return_type): def request_template(self): return io.CompositeRequestTemplate(self._request_templates) + @property + def url(self): + return utils.urlparse.urljoin(self.base_url, self.relative_url) + def add_transaction_hook(self, hook): self._transaction_hooks.append(hook) From fd007f5ab0b572c8a33f5759d066c960caff8c5f Mon Sep 17 00:00:00 2001 From: "P. Raj Kumar" Date: Sat, 24 Aug 2019 14:08:44 -0700 Subject: [PATCH 11/19] Fix broken Travis CI build The build failed because differences between how Schema.load() and Schema.dump() behave between Marshmallow v3 and v2. Marshmallow v3 released on August 18, after which our builds starting picking up the newest major release of the library, which our tests were not ready for. --- tests/unit/test_converters.py | 39 ++++++++++++++++++++++++------- uplink/converters/marshmallow_.py | 23 +++++++++--------- 2 files changed, 42 insertions(+), 20 deletions(-) diff --git a/tests/unit/test_converters.py b/tests/unit/test_converters.py index 60ca8099..1c859824 100644 --- a/tests/unit/test_converters.py +++ b/tests/unit/test_converters.py @@ -132,6 +132,23 @@ def __new__(cls, *args, **kwargs): class TestMarshmallowConverter(object): + for_marshmallow_2_and_3 = pytest.mark.parametrize( + "is_marshmallow_3", [True, False] + ) + + @staticmethod + def _mock_data(mocker, expected_result, is_marshmallow_3): + """Mocks the result of Schema.dump() or Schema.load()""" + + if is_marshmallow_3: + # After marshmallow 3.0, Schema.load() and Schema.dump() don't + # return a (data, errors) tuple any more. Only `data` is returned. + return expected_result + + result = mocker.Mock() + result.data = expected_result + return result + def test_init_without_marshmallow(self): old_marshmallow = converters.MarshmallowConverter.marshmallow converters.MarshmallowConverter.marshmallow = None @@ -139,16 +156,18 @@ def test_init_without_marshmallow(self): converters.MarshmallowConverter() converters.MarshmallowConverter.marshmallow = old_marshmallow + @for_marshmallow_2_and_3 def test_create_request_body_converter( - self, mocker, schema_mock_and_argument + self, mocker, schema_mock_and_argument, is_marshmallow_3 ): # Setup schema_mock, argument = schema_mock_and_argument expected_result = "data" - dump_result = mocker.Mock() - dump_result.data = expected_result - schema_mock.dump.return_value = dump_result + schema_mock.dump.return_value = self._mock_data( + mocker, expected_result, is_marshmallow_3 + ) converter = converters.MarshmallowConverter() + converter.is_marshmallow_3 = is_marshmallow_3 request_body = {"id": 0} # Run @@ -169,16 +188,18 @@ def test_create_request_body_converter_without_schema(self): # Verify assert c is None + @for_marshmallow_2_and_3 def test_create_response_body_converter( - self, mocker, schema_mock_and_argument + self, mocker, schema_mock_and_argument, is_marshmallow_3 ): # Setup schema_mock, argument = schema_mock_and_argument expected_result = "data" - load_result = mocker.Mock() - load_result.data = expected_result - schema_mock.load.return_value = load_result + schema_mock.load.return_value = self._mock_data( + mocker, expected_result, is_marshmallow_3 + ) converter = converters.MarshmallowConverter() + converter.is_marshmallow_3 = is_marshmallow_3 response = mocker.Mock(spec=["json"]) c = converter.create_response_body_converter(argument) @@ -235,7 +256,7 @@ def test_create_string_converter(self, schema_mock_and_argument): # Verify assert c is None - def test_register(self, mocker): + def test_register(self): # Setup converter = converters.MarshmallowConverter old_marshmallow = converter.marshmallow diff --git a/uplink/converters/marshmallow_.py b/uplink/converters/marshmallow_.py index efc05a2c..7211c63f 100644 --- a/uplink/converters/marshmallow_.py +++ b/uplink/converters/marshmallow_.py @@ -8,12 +8,6 @@ from uplink.converters import interfaces, register_default_converter_factory -def _extract_data(m): - # After marshmallow 3.0, Schema().load and Schema().dump don't - # return a (data, errors) tuple any more. Only `data` is returned. - return m if MarshmallowConverter.is_marshmallow_3 else m.data - - class MarshmallowConverter(interfaces.Factory): """ A converter that serializes and deserializes values using @@ -51,7 +45,8 @@ def __init__(self): raise ImportError("No module named 'marshmallow'") class ResponseBodyConverter(interfaces.Converter): - def __init__(self, schema): + def __init__(self, extract_data, schema): + self._extract_data = extract_data self._schema = schema def convert(self, response): @@ -61,14 +56,15 @@ def convert(self, response): # Assume that the response is already json json = response - return _extract_data(self._schema.load(json)) + return self._extract_data(self._schema.load(json)) class RequestBodyConverter(interfaces.Converter): - def __init__(self, schema): + def __init__(self, extract_data, schema): + self._extract_data = extract_data self._schema = schema def convert(self, value): - return _extract_data(self._schema.dump(value)) + return self._extract_data(self._schema.dump(value)) @classmethod def _get_schema(cls, type_): @@ -78,6 +74,11 @@ def _get_schema(cls, type_): return type_ raise ValueError("Expected marshmallow.Scheme subclass or instance.") + def _extract_data(self, m): + # After marshmallow 3.0, Schema.load() and Schema.dump() don't + # return a (data, errors) tuple any more. Only `data` is returned. + return m if self.is_marshmallow_3 else m.data + def _make_converter(self, converter_cls, type_): try: # Try to generate schema instance from the given type. @@ -86,7 +87,7 @@ def _make_converter(self, converter_cls, type_): # Failure: the given type is not a `marshmallow.Schema`. return None else: - return converter_cls(schema) + return converter_cls(self._extract_data, schema) def create_request_body_converter(self, type_, *args, **kwargs): return self._make_converter(self.RequestBodyConverter, type_) From 7d03c21a73c530c87a552fe309f2322685cfa8de Mon Sep 17 00:00:00 2001 From: "P. Raj Kumar" Date: Thu, 10 Oct 2019 12:46:23 -0700 Subject: [PATCH 12/19] Upgrade to pytest v4.6.5 Our CI build failures are related to pytest-dev/pytest#5903. We initially restricted pytest up to v4.1 in 64952e3 because pytest_twisted wasn't behaving well with the latest version of pytest at the time. --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 8e2db553..b15e76ab 100644 --- a/setup.py +++ b/setup.py @@ -27,7 +27,7 @@ def read(filename): # Twisted 19.7.0 dropped py3.4 support "twisted:python_version == '3.4'": "twisted<=19.2.1", "typing": ["typing>=3.6.4"], - "tests": ["pytest<4.1", "pytest-mock", "pytest-cov", "pytest-twisted"], + "tests": ["pytest==4.6.5", "pytest-mock", "pytest-cov", "pytest-twisted"], } metadata = { From 3344b3b8ac3e0d7608134135c16039261e49564f Mon Sep 17 00:00:00 2001 From: "P. Raj Kumar" Date: Thu, 26 Dec 2019 19:41:54 -0800 Subject: [PATCH 13/19] Run tests against a clean virtual environment --- tests/unit/test_utils.py | 2 +- tox.ini | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py index 9da88163..6fa1a7d0 100644 --- a/tests/unit/test_utils.py +++ b/tests/unit/test_utils.py @@ -43,4 +43,4 @@ def test_remaining_variables(self): builder = utils.URIBuilder("{variable}") assert builder.remaining_variables() == set(["variable"]) builder.set_variable(variable="resource") - assert builder.remaining_variables() == set() + assert len(builder.remaining_variables()) == 0 diff --git a/tox.ini b/tox.ini index b0a8b28e..46f49c85 100644 --- a/tox.ini +++ b/tox.ini @@ -13,3 +13,4 @@ commands = pipenv install --skip-lock --cov-config .coveragerc \ --cov=uplink \ {posargs} # Substitute with tox positional arguments +setenv = PIPENV_IGNORE_VIRTUALENVS = 1 From 4d81d4968fe915d7e4c299199644d82ac40db922 Mon Sep 17 00:00:00 2001 From: "P. Raj Kumar" Date: Thu, 26 Dec 2019 19:52:46 -0800 Subject: [PATCH 14/19] Explain PIPENV_IGNORE_VIRTUALENVS usage in tox.ini --- tox.ini | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tox.ini b/tox.ini index 46f49c85..4e93a321 100644 --- a/tox.ini +++ b/tox.ini @@ -13,4 +13,8 @@ commands = pipenv install --skip-lock --cov-config .coveragerc \ --cov=uplink \ {posargs} # Substitute with tox positional arguments + +# Ensure pipenv runs the tests against a clean virtual environment, +# for better parity between Travis CI builds and testing on a local +# machine during development. setenv = PIPENV_IGNORE_VIRTUALENVS = 1 From 6b889476358b35fb5372edafdc9f04b215831834 Mon Sep 17 00:00:00 2001 From: "P. Raj Kumar" Date: Tue, 31 Dec 2019 19:56:59 -0800 Subject: [PATCH 15/19] Add Python 3.7 and 3.8 Travis builds (#186) * Add Python 3.7 build to Travis config * Remove unnecessary commands from tox.ini For local development, setting up tox to use pipenv to run tests against two different versions of Python seems unnecessary, since the CI builds will run against the versions we want already. Further, there are some known constraints with running pipenv in a multi-python environment and the cost of getting this to work doesn't seem worth it. * Add Python 3.8 Travis build --- .travis.yml | 4 ++-- Pipfile | 1 - tox.ini | 10 ---------- uplink/auth.py | 12 ++++++++++-- 4 files changed, 12 insertions(+), 15 deletions(-) diff --git a/.travis.yml b/.travis.yml index fa135e4e..ffbb2819 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,8 +4,8 @@ python: - '3.4' - '3.5' - '3.6' - - '3.6-dev' - - '3.7-dev' + - '3.7' + - '3.8' before_script: - pip install tox - if [[ $TRAVIS_PYTHON_VERSION == 3.6 ]]; then pip install flake8 flake8-bugbear; fi diff --git a/Pipfile b/Pipfile index 257ff7d4..e94a592c 100644 --- a/Pipfile +++ b/Pipfile @@ -3,7 +3,6 @@ url = "https://pypi.org/simple" verify_ssl = true name = "pypi" - [dev-packages] pre-commit = "*" tox = "*" diff --git a/tox.ini b/tox.ini index 4e93a321..19b1c350 100644 --- a/tox.ini +++ b/tox.ini @@ -1,11 +1,6 @@ # Configuration file for Tox, a general virtualenv managment tool. # Website (June 2016): http://tox.readthedocs.io/en/latest/ -[tox] -# These are all "default" test environments provided by Tox. -# They are automatically created upon invoking the "tox" CLI. -envlist = py27, py36 - [testenv] deps = pipenv commands = pipenv install --skip-lock @@ -13,8 +8,3 @@ commands = pipenv install --skip-lock --cov-config .coveragerc \ --cov=uplink \ {posargs} # Substitute with tox positional arguments - -# Ensure pipenv runs the tests against a clean virtual environment, -# for better parity between Travis CI builds and testing on a local -# machine during development. -setenv = PIPENV_IGNORE_VIRTUALENVS = 1 diff --git a/uplink/auth.py b/uplink/auth.py index a5946565..d5971ca1 100644 --- a/uplink/auth.py +++ b/uplink/auth.py @@ -55,6 +55,7 @@ def __init__(self, token): token_param = ExampleApiTokenParam(TOKEN) api_consumer = SomeApiConsumerClass(BASE_URL, auth=token_param) """ + def __init__(self, param, token): self._param = param self._param_value = token @@ -97,6 +98,7 @@ def __init__(self, token): token_header = ExampleApiTokenHeader(TOKEN) api_consumer = SomeApiConsumerClass(BASE_URL, auth=token_header) """ + _header = None _prefix = None @@ -161,6 +163,7 @@ class ProxyAuth(BasicAuth): ) github = GitHub(BASE_URL, auth=auth_methods) """ + _header = "Proxy-Authorization" @@ -226,8 +229,11 @@ class MultiAuth(object): for method in auth_methods: print(method.__class__.__name__) """ + def __init__(self, *auth_methods): - self._auth_methods = [get_auth(auth_method) for auth_method in auth_methods] + self._auth_methods = [ + get_auth(auth_method) for auth_method in auth_methods + ] def __call__(self, request_builder): for auth_method in self._auth_methods: @@ -243,4 +249,6 @@ def append(self, auth_method): self._auth_methods.append(get_auth(auth_method)) def extend(self, auth_methods): - self._auth_methods.extend([get_auth(auth_method) for auth_method in auth_methods]) + self._auth_methods.extend( + [get_auth(auth_method) for auth_method in auth_methods] + ) From a61edb2c509ba6ca311c427f26810ed54b038349 Mon Sep 17 00:00:00 2001 From: "P. Raj Kumar" Date: Thu, 2 Jan 2020 08:46:19 -0800 Subject: [PATCH 16/19] Migrate repo to travis-ci.com --- README.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index 6c53a869..c685d526 100644 --- a/README.rst +++ b/README.rst @@ -180,8 +180,8 @@ Want to report a bug, request a feature, or contribute code to Uplink? Checkout the `Contribution Guide`_ for where to start. Thank you for taking the time to improve an open source project :purple_heart: -.. |Build Status| image:: https://travis-ci.org/prkumar/uplink.svg?branch=master - :target: https://travis-ci.org/prkumar/uplink +.. |Build Status| image:: https://travis-ci.com/prkumar/uplink.svg?branch=master + :target: https://travis-ci.com/prkumar/uplink .. |Code Climate| image:: https://api.codeclimate.com/v1/badges/d5c5666134763ff1d6c0/maintainability :target: https://codeclimate.com/github/prkumar/uplink/maintainability :alt: Maintainability From d14509a0ff13854ae93924e6e14fc238f43cdecb Mon Sep 17 00:00:00 2001 From: Alexander Duryagin Date: Fri, 10 Jan 2020 18:30:41 +0300 Subject: [PATCH 17/19] Exclude tests subpackages from Python package --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index b15e76ab..404ababc 100644 --- a/setup.py +++ b/setup.py @@ -53,7 +53,7 @@ def read(filename): "Programming Language :: Python :: Implementation :: PyPy", ], "keywords": "http api rest client retrofit", - "packages": find_packages(exclude=("tests",)), + "packages": find_packages(exclude=("tests", "tests.*")), "install_requires": install_requires, "extras_require": extras_require, } From 8ded9435d6332edd7a0aae1b41e3317573ac0dad Mon Sep 17 00:00:00 2001 From: "P. Raj Kumar" Date: Sat, 8 Feb 2020 00:23:49 -0800 Subject: [PATCH 18/19] Update CHANGELOG for v0.9.1 --- CHANGELOG.rst | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 5e2c66a6..38bb4b32 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -6,12 +6,19 @@ 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. -Unreleased_ -=========== -Changed -------- +0.9.1_ - 2020-02-08 +=================== +Fixed +----- - Omit ``Header`` argument from request when its value is ``None``. (`#167`_, `#169`_) +- Fix ``AttributeError`` raised on usage of ``uplink.Url``. + (`#164`_, `#165`_ by `@cognifloyd`_) + +Changed +------- +- Exclude ``tests`` subpackages from wheel. + (`#188`_ by `@daa`_) 0.9.0_ - 2019-06-05 =================== @@ -301,7 +308,7 @@ Added .. _`Semantic Versioning`: https://packaging.python.org/tutorials/distributing-packages/#semantic-versioning-preferred .. Releases -.. _Unreleased: https://github.com/prkumar/uplink/compare/v0.9.0...HEAD +.. _0.9.1: https://github.com/prkumar/uplink/compare/v0.9.1...HEAD .. _0.9.0: https://github.com/prkumar/uplink/compare/v0.8.0...v0.9.0 .. _0.8.0: https://github.com/prkumar/uplink/compare/v0.7.0...v0.8.0 .. _0.7.0: https://github.com/prkumar/uplink/compare/v0.6.1...v0.7.0 @@ -334,6 +341,8 @@ Added .. _#154: https://github.com/prkumar/uplink/pull/154 .. _#155: https://github.com/prkumar/uplink/pull/155 .. _#159: https://github.com/prkumar/uplink/pull/159 +.. _#164: https://github.com/prkumar/uplink/pull/164 +.. _#165: https://github.com/prkumar/uplink/pull/165 .. _#167: https://github.com/prkumar/uplink/issues/167 .. _#169: https://github.com/prkumar/uplink/pull/169 @@ -343,3 +352,4 @@ Added .. _@brandonio21: https://github.com/brandonio21 .. _@itstehkman: https://github.com/itstehkman .. _@kadrach: https://github.com/kadrach +.. _@cognifloyd: https://github.com/cognifloyd From a6a9bcd0b4426e8a7f1369f0689c310b3bdae824 Mon Sep 17 00:00:00 2001 From: "P. Raj Kumar" Date: Sat, 8 Feb 2020 00:26:39 -0800 Subject: [PATCH 19/19] Fix broken link in CHANGELOG --- CHANGELOG.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 38bb4b32..b13e5eae 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -341,10 +341,11 @@ Added .. _#154: https://github.com/prkumar/uplink/pull/154 .. _#155: https://github.com/prkumar/uplink/pull/155 .. _#159: https://github.com/prkumar/uplink/pull/159 -.. _#164: https://github.com/prkumar/uplink/pull/164 +.. _#164: https://github.com/prkumar/uplink/issues/164 .. _#165: https://github.com/prkumar/uplink/pull/165 .. _#167: https://github.com/prkumar/uplink/issues/167 .. _#169: https://github.com/prkumar/uplink/pull/169 +.. _#188: https://github.com/prkumar/uplink/pull/188 .. Contributors .. _@daa: https://github.com/daa