From 24e7d43a16a77c544244255dbcf09e53f2b16b84 Mon Sep 17 00:00:00 2001 From: Jamie Lennox Date: Tue, 1 Jul 2014 12:50:56 +1000 Subject: [PATCH] Allow custom matchers Allow users to specify there own matchers. A matcher should take a request and return a response or None. --- docs/matching.rst | 36 +++++++++ requests_mock/adapter.py | 20 +++-- requests_mock/mocker.py | 1 + requests_mock/tests/test_custom_matchers.py | 82 +++++++++++++++++++++ 4 files changed, 134 insertions(+), 5 deletions(-) create mode 100644 requests_mock/tests/test_custom_matchers.py diff --git a/docs/matching.rst b/docs/matching.rst index c8eee55..1b8d9e3 100644 --- a/docs/matching.rst +++ b/docs/matching.rst @@ -167,3 +167,39 @@ Only the headers that are provided need match, any additional headers will be ig Traceback (most recent call last): ... requests_mock.exceptions.NoMockAddress: No mock address: POST mock://test.com/headers + + +Custom Matching +=============== + +Internally calling :py:meth:`~requests_mock.Adapter.register_uri` creates a *matcher* object for you and adds it to the list of matchers to check against. + +A *matcher* is any callable that takes a :py:class:`requests.Request` and returns a :py:class:`requests.Response` on a successful match or *None* if it does not handle the request. + +If you need more flexibility than provided by :py:meth:`~requests_mock.Adapter.register_uri` then you can add your own *matcher* to the :py:class:`~requests_mock.Adapter`. Custom *matchers* can be used in conjunction with the inbuilt *matchers*. If a matcher returns *None* then the request will be passed to the next *matcher* as with using :py:meth:`~requests_mock.Adapter.register_uri`. + +.. doctest:: + :hide: + + >>> import requests + >>> import requests_mock + >>> adapter = requests_mock.Adapter() + >>> session = requests.Session() + >>> session.mount('mock', adapter) + +.. doctest:: + + >>> def custom_matcher(request): + ... if request.path_url == '/test': + ... resp = requests.Response() + ... resp.status_code = 200 + ... return resp + ... return None + ... + >>> adapter.add_matcher(custom_matcher) + >>> session.get('mock://test.com/test').status_code + 200 + >>> session.get('mock://test.com/other') + Traceback (most recent call last): + ... + requests_mock.exceptions.NoMockAddress: No mock address: POST mock://test.com/other diff --git a/requests_mock/adapter.py b/requests_mock/adapter.py index 15c64e6..799aafa 100644 --- a/requests_mock/adapter.py +++ b/requests_mock/adapter.py @@ -260,11 +260,21 @@ class Adapter(BaseAdapter): response_list = [kwargs] responses = [_MatcherResponse(**k) for k in response_list] - self._matchers.append(_Matcher(method, - url, - responses, - complete_qs=complete_qs, - request_headers=request_headers)) + self.add_matcher(_Matcher(method, + url, + responses, + complete_qs=complete_qs, + request_headers=request_headers)) + + def add_matcher(self, matcher): + """Register a custom matcher. + + A matcher is a callable that takes a `requests.Request` and returns a + `requests.Response` if it matches or None if not. + + :param callable matcher: The matcher to execute. + """ + self._matchers.append(matcher) @property def last_request(self): diff --git a/requests_mock/mocker.py b/requests_mock/mocker.py index 190693a..3355b73 100644 --- a/requests_mock/mocker.py +++ b/requests_mock/mocker.py @@ -27,6 +27,7 @@ class MockerCore(object): _PROXY_FUNCS = set(['last_request', 'register_uri', + 'add_matcher', 'request_history']) def __init__(self, **kwargs): diff --git a/requests_mock/tests/test_custom_matchers.py b/requests_mock/tests/test_custom_matchers.py new file mode 100644 index 0000000..f0d29da --- /dev/null +++ b/requests_mock/tests/test_custom_matchers.py @@ -0,0 +1,82 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import requests +import six + +import requests_mock +from requests_mock.tests import base + + +class FailMatcher(object): + + def __init___(self): + self.called = False + + def __call__(self, request): + self.called = True + return None + + +def match_all(request): + resp = requests.Response() + resp.status_code = 200 + resp._content = six.b('data') + resp.request = request + resp.encoding = 'utf-8' + resp.close = lambda *args, **kwargs: None + + return resp + + +def test_a(request): + if 'a' in request.url: + return match_all(request) + + return None + + +class CustomMatchersTests(base.TestCase): + + def assertMatchAll(self, resp): + self.assertEqual(200, resp.status_code) + self.assertEqual(resp.text, six.u('data')) + + @requests_mock.Mocker() + def test_custom_matcher(self, mocker): + mocker.add_matcher(match_all) + + resp = requests.get('http://any/thing') + self.assertMatchAll(resp) + + @requests_mock.Mocker() + def test_failing_matcher(self, mocker): + failer = FailMatcher() + + mocker.add_matcher(match_all) + mocker.add_matcher(failer) + + resp = requests.get('http://any/thing') + + self.assertMatchAll(resp) + self.assertTrue(failer.called) + + @requests_mock.Mocker() + def test_some_pass(self, mocker): + mocker.add_matcher(test_a) + + resp = requests.get('http://any/thing') + self.assertMatchAll(resp) + + self.assertRaises(requests_mock.NoMockAddress, + requests.get, + 'http://other/thing')