[Python-modules-commits] [vcr.py] 01/05: Imported Upstream version 1.7.3

Daniel Stender danstender-guest at moszumanska.debian.org
Wed Oct 7 19:00:40 UTC 2015


This is an automated email from the git hooks/post-receive script.

danstender-guest pushed a commit to branch master
in repository vcr.py.

commit ed4328cff1c291912cc0927bc5e7db99dbfd8b3d
Author: Daniel Stender <debian at danielstender.com>
Date:   Sun Sep 6 22:20:35 2015 +0200

    Imported Upstream version 1.7.3
---
 README.rst                                         |  52 +++++++----
 setup.py                                           |  21 ++++-
 tests/integration/test_httplib2.py                 |  11 +++
 tests/integration/test_requests.py                 |   9 ++
 tests/integration/test_tornado.py                  |  78 ++++++++++++++++
 .../test_tornado_exception_can_be_caught.yaml      |  62 +++++++++++++
 .../test_tornado_with_decorator_use_cassette.yaml  |  53 +++++++++++
 tests/integration/test_urllib2.py                  |   9 ++
 tests/unit/test_cassettes.py                       |  34 +++++++
 tests/unit/test_serialize.py                       |  50 ++++++++++
 tests/unit/test_vcr.py                             |  70 +++++++++++++-
 vcr/cassette.py                                    | 101 ++++++++++++++-------
 vcr/config.py                                      |  73 +++++++++------
 vcr/errors.py                                      |   5 +-
 vcr/matchers.py                                    |  11 ++-
 vcr/patch.py                                       |  40 +++++---
 vcr/request.py                                     |  10 +-
 vcr/stubs/tornado_stubs.py                         |  73 ++++-----------
 18 files changed, 591 insertions(+), 171 deletions(-)

diff --git a/README.rst b/README.rst
index ac35d85..5122a62 100644
--- a/README.rst
+++ b/README.rst
@@ -14,17 +14,18 @@ library <https://github.com/vcr/vcr>`__.
 What it does
 ------------
 
-VCR.py simplifies and speeds up tests that make HTTP requests. The first
-time you run code that is inside a VCR.py context manager or decorated
-function, VCR.py records all HTTP interactions that take place through
-the libraries it supports and serializes and writes them to a flat file
-(in yaml format by default). This flat file is called a cassette. When
-the relevant peice of code is executed again, VCR.py will read the
-serialized requests and responses from the aforementioned cassette file,
-and intercept any HTTP requests that it recognizes from the original
-test run and return responses that corresponded to those requests. This
-means that the requests will not actually result in HTTP traffic, which
-confers several benefits including:
+VCR.py simplifies and speeds up tests that make HTTP requests. The
+first time you run code that is inside a VCR.py context manager or
+decorated function, VCR.py records all HTTP interactions that take
+place through the libraries it supports and serializes and writes them
+to a flat file (in yaml format by default). This flat file is called a
+cassette. When the relevant peice of code is executed again, VCR.py
+will read the serialized requests and responses from the
+aforementioned cassette file, and intercept any HTTP requests that it
+recognizes from the original test run and return the responses that
+corresponded to those requests. This means that the requests will not
+actually result in HTTP traffic, which confers several benefits
+including:
 
 -  The ability to work offline
 -  Completely deterministic tests
@@ -50,6 +51,7 @@ The following http libraries are supported:
 -  requests (both 1.x and 2.x versions)
 -  httplib2
 -  boto
+-  Tornado's AsyncHTTPClient
 
 Usage
 -----
@@ -108,10 +110,10 @@ If you don't like VCR's defaults, you can set options by instantiating a
     import vcr
 
     my_vcr = vcr.VCR(
-        serializer = 'json',
-        cassette_library_dir = 'fixtures/cassettes',
-        record_mode = 'once',
-        match_on = ['uri', 'method'],
+        serializer='json',
+        cassette_library_dir='fixtures/cassettes',
+        record_mode='once',
+        match_on=['uri', 'method'],
     )
 
     with my_vcr.use_cassette('test.json'):
@@ -416,12 +418,13 @@ that of ``before_record``:
 .. code:: python
 
     def scrub_string(string, replacement=''):
-        def before_record_reponse(response):
-            return response['body']['string'] = response['body']['string'].replace(string, replacement)
-        return scrub_string
+        def before_record_response(response):
+            response['body']['string'] = response['body']['string'].replace(string, replacement)
+            return response
+        return before_record_response
 
     my_vcr = vcr.VCR(
-        before_record=scrub_string(settings.USERNAME, 'username'),
+        before_record_response=scrub_string(settings.USERNAME, 'username'),
     )
     with my_vcr.use_cassette('test.yml'):
          # your http code here    
@@ -606,6 +609,17 @@ new API in version 1.0.x
 
 Changelog
 ---------
+-  1.7.3 [#188] ``additional_matchers`` kwarg on ``use_casstte``.
+   [#191] Actually support passing multiple before_record_request
+   functions (thanks @agriffis).
+-  1.7.2 [#186] Get effective_url in tornado (thanks @mvschaik), [#187]
+   Set request_time on Response object in tornado (thanks @abhinav).
+-  1.7.1 [#183] Patch ``fetch_impl`` instead of the entire HTTPClient
+   class for Tornado (thanks @abhinav).
+-  1.7.0 [#177] Properly support coroutine/generator decoration. [#178]
+   Support distribute (thanks @graingert). [#163] Make compatibility
+   between python2 and python3 recorded cassettes more robust (thanks
+   @gward).
 -  1.6.1 [#169] Support conditional requirements in old versions of
    pip, Fix RST parse errors generated by pandoc, [Tornado] Fix
    unsupported features exception not being raised, [#166]
diff --git a/setup.py b/setup.py
index 826744f..9e77760 100644
--- a/setup.py
+++ b/setup.py
@@ -1,12 +1,15 @@
 #!/usr/bin/env python
 
 import sys
+import logging
+
 from setuptools import setup, find_packages
 from setuptools.command.test import test as TestCommand
 import pkg_resources
 
 long_description = open('README.rst', 'r').read()
 
+
 class PyTest(TestCommand):
 
     def finalize_options(self):
@@ -21,7 +24,7 @@ class PyTest(TestCommand):
         sys.exit(errno)
 
 
-install_requires=['PyYAML', 'wrapt', 'six>=1.5']
+install_requires = ['PyYAML', 'wrapt', 'six>=1.5']
 
 
 extras_require = {
@@ -31,14 +34,24 @@ extras_require = {
 }
 
 
-if 'bdist_wheel' not in sys.argv:
+try:
+    if 'bdist_wheel' not in sys.argv:
+        for key, value in extras_require.items():
+            if key.startswith(':') and pkg_resources.evaluate_marker(key[1:]):
+                install_requires.extend(value)
+except Exception:
+    logging.getLogger(__name__).exception(
+        'Something went wrong calculating platform specific dependencies, so '
+        "you're getting them all!"
+    )
     for key, value in extras_require.items():
-        if key.startswith(':') and pkg_resources.evaluate_marker(key[1:]):
+        if key.startswith(':'):
             install_requires.extend(value)
 
+
 setup(
     name='vcrpy',
-    version='1.6.1',
+    version='1.7.3',
     description=(
         "Automatically mock your HTTP interactions to simplify and "
         "speed up testing"
diff --git a/tests/integration/test_httplib2.py b/tests/integration/test_httplib2.py
index f3ab42c..f830a06 100644
--- a/tests/integration/test_httplib2.py
+++ b/tests/integration/test_httplib2.py
@@ -56,6 +56,17 @@ def test_response_headers(scheme, tmpdir):
         resp, _ = httplib2.Http().request(url)
         assert set(headers) == set(resp.items())
 
+def test_effective_url(scheme, tmpdir):
+    '''Ensure that the effective_url is captured'''
+    url = scheme + '://httpbin.org/redirect-to?url=/html'
+    with vcr.use_cassette(str(tmpdir.join('headers.yaml'))) as cass:
+        resp, _ = httplib2.Http().request(url)
+        effective_url = resp['content-location']
+        assert effective_url == scheme + '://httpbin.org/html'
+
+    with vcr.use_cassette(str(tmpdir.join('headers.yaml'))) as cass:
+        resp, _ = httplib2.Http().request(url)
+        assert effective_url == resp['content-location']
 
 def test_multiple_requests(scheme, tmpdir):
     '''Ensure that we can cache multiple requests'''
diff --git a/tests/integration/test_requests.py b/tests/integration/test_requests.py
index 79e74b8..674f6ed 100644
--- a/tests/integration/test_requests.py
+++ b/tests/integration/test_requests.py
@@ -44,6 +44,15 @@ def test_body(tmpdir, scheme):
     with vcr.use_cassette(str(tmpdir.join('body.yaml'))):
         assert content == requests.get(url).content
 
+def test_effective_url(scheme, tmpdir):
+    '''Ensure that the effective_url is captured'''
+    url = scheme + '://httpbin.org/redirect-to?url=/html'
+    with vcr.use_cassette(str(tmpdir.join('url.yaml'))):
+        effective_url = requests.get(url).url
+        assert effective_url == scheme + '://httpbin.org/html'
+
+    with vcr.use_cassette(str(tmpdir.join('url.yaml'))):
+        assert effective_url == requests.get(url).url
 
 def test_auth(tmpdir, scheme):
     '''Ensure that we can handle basic auth'''
diff --git a/tests/integration/test_tornado.py b/tests/integration/test_tornado.py
index 1230077..b7d3c4e 100644
--- a/tests/integration/test_tornado.py
+++ b/tests/integration/test_tornado.py
@@ -81,6 +81,17 @@ def test_body(get_client, tmpdir, scheme):
         assert content == (yield get(get_client(), url)).body
         assert 1 == cass.play_count
 
+ at pytest.mark.gen_test
+def test_effective_url(get_client, scheme, tmpdir):
+    '''Ensure that the effective_url is captured'''
+    url = scheme + '://httpbin.org/redirect-to?url=/html'
+    with vcr.use_cassette(str(tmpdir.join('url.yaml'))):
+        effective_url = (yield get(get_client(), url)).effective_url
+        assert effective_url == scheme + '://httpbin.org/html'
+
+    with vcr.use_cassette(str(tmpdir.join('url.yaml'))) as cass:
+        assert effective_url == (yield get(get_client(), url)).effective_url
+        assert 1 == cass.play_count
 
 @pytest.mark.gen_test
 def test_auth(get_client, tmpdir, scheme):
@@ -275,3 +286,70 @@ def test_cannot_overwrite_cassette_raise_error_disabled(get_client, tmpdir):
         )
 
     assert isinstance(response.error, CannotOverwriteExistingCassetteException)
+
+
+ at pytest.mark.gen_test
+ at vcr.use_cassette(path_transformer=vcr.default_vcr.ensure_suffix('.yaml'))
+def test_tornado_with_decorator_use_cassette(get_client):
+    response = yield get_client().fetch(
+        http.HTTPRequest('http://www.google.com/', method='GET')
+    )
+    assert response.body.decode('utf-8') == "not actually google"
+
+
+ at pytest.mark.gen_test
+ at vcr.use_cassette(path_transformer=vcr.default_vcr.ensure_suffix('.yaml'))
+def test_tornado_exception_can_be_caught(get_client):
+    try:
+        yield get(get_client(), 'http://httpbin.org/status/500')
+    except http.HTTPError as e:
+        assert e.code == 500
+
+    try:
+        yield get(get_client(), 'http://httpbin.org/status/404')
+    except http.HTTPError as e:
+        assert e.code == 404
+
+
+ at pytest.mark.gen_test
+def test_existing_references_get_patched(tmpdir):
+    from tornado.httpclient import AsyncHTTPClient
+
+    with vcr.use_cassette(str(tmpdir.join('data.yaml'))):
+        client = AsyncHTTPClient()
+        yield get(client, 'http://httpbin.org/get')
+
+    with vcr.use_cassette(str(tmpdir.join('data.yaml'))) as cass:
+        yield get(client, 'http://httpbin.org/get')
+        assert cass.play_count == 1
+
+
+ at pytest.mark.gen_test
+def test_existing_instances_get_patched(get_client, tmpdir):
+    '''Ensure that existing instances of AsyncHTTPClient get patched upon
+    entering VCR context.'''
+
+    client = get_client()
+
+    with vcr.use_cassette(str(tmpdir.join('data.yaml'))):
+        yield get(client, 'http://httpbin.org/get')
+
+    with vcr.use_cassette(str(tmpdir.join('data.yaml'))) as cass:
+        yield get(client, 'http://httpbin.org/get')
+        assert cass.play_count == 1
+
+
+ at pytest.mark.gen_test
+def test_request_time_is_set(get_client, tmpdir):
+    '''Ensures that the request_time on HTTPResponses is set.'''
+
+    with vcr.use_cassette(str(tmpdir.join('data.yaml'))):
+        client = get_client()
+        response = yield get(client, 'http://httpbin.org/get')
+        assert response.request_time is not None
+
+    with vcr.use_cassette(str(tmpdir.join('data.yaml'))) as cass:
+        client = get_client()
+        response = yield get(client, 'http://httpbin.org/get')
+        assert response.request_time is not None
+        assert cass.play_count == 1
diff --git a/tests/integration/test_tornado_exception_can_be_caught.yaml b/tests/integration/test_tornado_exception_can_be_caught.yaml
new file mode 100644
index 0000000..c88f1f0
--- /dev/null
+++ b/tests/integration/test_tornado_exception_can_be_caught.yaml
@@ -0,0 +1,62 @@
+interactions:
+- request:
+    body: null
+    headers: {}
+    method: GET
+    uri: http://httpbin.org/status/500
+  response:
+    body: {string: !!python/unicode ''}
+    headers:
+    - !!python/tuple
+      - Content-Length
+      - ['0']
+    - !!python/tuple
+      - Server
+      - [nginx]
+    - !!python/tuple
+      - Connection
+      - [close]
+    - !!python/tuple
+      - Access-Control-Allow-Credentials
+      - ['true']
+    - !!python/tuple
+      - Date
+      - ['Thu, 30 Jul 2015 17:32:39 GMT']
+    - !!python/tuple
+      - Access-Control-Allow-Origin
+      - ['*']
+    - !!python/tuple
+      - Content-Type
+      - [text/html; charset=utf-8]
+    status: {code: 500, message: INTERNAL SERVER ERROR}
+- request:
+    body: null
+    headers: {}
+    method: GET
+    uri: http://httpbin.org/status/404
+  response:
+    body: {string: !!python/unicode ''}
+    headers:
+    - !!python/tuple
+      - Content-Length
+      - ['0']
+    - !!python/tuple
+      - Server
+      - [nginx]
+    - !!python/tuple
+      - Connection
+      - [close]
+    - !!python/tuple
+      - Access-Control-Allow-Credentials
+      - ['true']
+    - !!python/tuple
+      - Date
+      - ['Thu, 30 Jul 2015 17:32:39 GMT']
+    - !!python/tuple
+      - Access-Control-Allow-Origin
+      - ['*']
+    - !!python/tuple
+      - Content-Type
+      - [text/html; charset=utf-8]
+    status: {code: 404, message: NOT FOUND}
+version: 1
diff --git a/tests/integration/test_tornado_with_decorator_use_cassette.yaml b/tests/integration/test_tornado_with_decorator_use_cassette.yaml
new file mode 100644
index 0000000..ae05aca
--- /dev/null
+++ b/tests/integration/test_tornado_with_decorator_use_cassette.yaml
@@ -0,0 +1,53 @@
+interactions:
+- request:
+    body: null
+    headers: {}
+    method: GET
+    uri: http://www.google.com/
+  response:
+    body: {string: !!python/unicode 'not actually google'}
+    headers:
+    - !!python/tuple
+      - Expires
+      - ['-1']
+    - !!python/tuple
+      - Connection
+      - [close]
+    - !!python/tuple
+      - P3p
+      - ['CP="This is not a P3P policy! See http://www.google.com/support/accounts/bin/answer.py?hl=en&answer=151657
+          for more info."']
+    - !!python/tuple
+      - Alternate-Protocol
+      - ['80:quic,p=0']
+    - !!python/tuple
+      - Accept-Ranges
+      - [none]
+    - !!python/tuple
+      - X-Xss-Protection
+      - [1; mode=block]
+    - !!python/tuple
+      - Vary
+      - [Accept-Encoding]
+    - !!python/tuple
+      - Date
+      - ['Thu, 30 Jul 2015 08:41:40 GMT']
+    - !!python/tuple
+      - Cache-Control
+      - ['private, max-age=0']
+    - !!python/tuple
+      - Content-Type
+      - [text/html; charset=ISO-8859-1]
+    - !!python/tuple
+      - Set-Cookie
+      - ['PREF=ID=1111111111111111:FF=0:TM=1438245700:LM=1438245700:V=1:S=GAzVO0ALebSpC_cJ;
+          expires=Sat, 29-Jul-2017 08:41:40 GMT; path=/; domain=.google.com', 'NID=69=Br7oRAwgmKoK__HC6FEnuxglTFDmFxqP6Md63lKhzW1w6WkDbp3U90CDxnUKvDP6wJH8yxY5Lk5ZnFf66Q1B0d4OsYoKgq0vjfBAYXuCIAWtOuGZEOsFXanXs7pt2Mjx;
+          expires=Fri, 29-Jan-2016 08:41:40 GMT; path=/; domain=.google.com; HttpOnly']
+    - !!python/tuple
+      - X-Frame-Options
+      - [SAMEORIGIN]
+    - !!python/tuple
+      - Server
+      - [gws]
+    status: {code: 200, message: OK}
+version: 1
diff --git a/tests/integration/test_urllib2.py b/tests/integration/test_urllib2.py
index 484001c..6754303 100644
--- a/tests/integration/test_urllib2.py
+++ b/tests/integration/test_urllib2.py
@@ -49,6 +49,15 @@ def test_response_headers(scheme, tmpdir):
         open2 = urlopen(url).info().items()
         assert sorted(open1) == sorted(open2)
 
+def test_effective_url(scheme, tmpdir):
+    '''Ensure that the effective_url is captured'''
+    url = scheme + '://httpbin.org/redirect-to?url=/html'
+    with vcr.use_cassette(str(tmpdir.join('headers.yaml'))) as cass:
+        effective_url = urlopen(url).geturl()
+        assert effective_url == scheme + '://httpbin.org/html'
+
+    with vcr.use_cassette(str(tmpdir.join('headers.yaml'))) as cass:
+        assert effective_url == urlopen(url).geturl()
 
 def test_multiple_requests(scheme, tmpdir):
     '''Ensure that we can cache multiple requests'''
diff --git a/tests/unit/test_cassettes.py b/tests/unit/test_cassettes.py
index e7d8b7c..e1e9fb9 100644
--- a/tests/unit/test_cassettes.py
+++ b/tests/unit/test_cassettes.py
@@ -253,3 +253,37 @@ def test_func_path_generator():
     def function_name(cassette):
         assert cassette._path == os.path.join(os.path.dirname(__file__), 'function_name')
     function_name()
+
+
+def test_use_as_decorator_on_coroutine():
+    original_http_connetion = httplib.HTTPConnection
+    @Cassette.use(inject=True)
+    def test_function(cassette):
+        assert httplib.HTTPConnection.cassette is cassette
+        assert httplib.HTTPConnection is not original_http_connetion
+        value = yield 1
+        assert value == 1
+        assert httplib.HTTPConnection.cassette is cassette
+        assert httplib.HTTPConnection is not original_http_connetion
+        value = yield 2
+        assert value == 2
+    coroutine = test_function()
+    value = next(coroutine)
+    while True:
+        try:
+            value = coroutine.send(value)
+        except StopIteration:
+            break
+
+
+def test_use_as_decorator_on_generator():
+    original_http_connetion = httplib.HTTPConnection
+    @Cassette.use(inject=True)
+    def test_function(cassette):
+        assert httplib.HTTPConnection.cassette is cassette
+        assert httplib.HTTPConnection is not original_http_connetion
+        yield 1
+        assert httplib.HTTPConnection.cassette is cassette
+        assert httplib.HTTPConnection is not original_http_connetion
+        yield 2
+    assert list(test_function()) == [1, 2]
diff --git a/tests/unit/test_serialize.py b/tests/unit/test_serialize.py
index 41d97bd..3e03adb 100644
--- a/tests/unit/test_serialize.py
+++ b/tests/unit/test_serialize.py
@@ -1,3 +1,4 @@
+# -*- encoding: utf-8 -*-
 import pytest
 
 from vcr.compat import mock
@@ -27,6 +28,55 @@ def test_deserialize_new_json_cassette():
         deserialize(f.read(), jsonserializer)
 
 
+REQBODY_TEMPLATE = u'''\
+interactions:
+- request:
+    body: {req_body}
+    headers:
+      Content-Type: [application/x-www-form-urlencoded]
+      Host: [httpbin.org]
+    method: POST
+    uri: http://httpbin.org/post
+  response:
+    body: {{string: ""}}
+    headers:
+      content-length: ['0']
+      content-type: [application/json]
+    status: {{code: 200, message: OK}}
+'''
+
+
+# A cassette generated under Python 2 stores the request body as a string,
+# but the same cassette generated under Python 3 stores it as "!!binary".
+# Make sure we accept both forms, regardless of whether we're running under
+# Python 2 or 3.
+ at pytest.mark.parametrize("req_body, expect", [
+    # Cassette written under Python 2 (pure ASCII body)
+    ('x=5&y=2', b'x=5&y=2'),
+    # Cassette written under Python 3 (pure ASCII body)
+    ('!!binary |\n      eD01Jnk9Mg==', b'x=5&y=2'),
+
+    # Request body has non-ASCII chars (x=föo&y=2), encoded in UTF-8.
+    ('!!python/str "x=f\\xF6o&y=2"', b'x=f\xc3\xb6o&y=2'),
+    ('!!binary |\n      eD1mw7ZvJnk9Mg==', b'x=f\xc3\xb6o&y=2'),
+
+    # Same request body, this time encoded in UTF-16. In this case, we
+    # write the same YAML file under both Python 2 and 3, so there's only
+    # one test case here.
+    ('!!binary |\n      //54AD0AZgD2AG8AJgB5AD0AMgA=',
+     b'\xff\xfex\x00=\x00f\x00\xf6\x00o\x00&\x00y\x00=\x002\x00'),
+
+    # Same again, this time encoded in ISO-8859-1.
+    ('!!binary |\n      eD1m9m8meT0y', b'x=f\xf6o&y=2'),
+])
+def test_deserialize_py2py3_yaml_cassette(tmpdir, req_body, expect):
+    cfile = tmpdir.join('test_cassette.yaml')
+    cfile.write(REQBODY_TEMPLATE.format(req_body=req_body))
+    with open(str(cfile)) as f:
+        (requests, responses) = deserialize(f.read(), yamlserializer)
+    assert requests[0].body == expect
+
+
 @mock.patch.object(jsonserializer.json, 'dumps',
                    side_effect=UnicodeDecodeError('utf-8', b'unicode error in serialization',
                                                   0, 10, 'blew up'))
diff --git a/tests/unit/test_vcr.py b/tests/unit/test_vcr.py
index 8b1de97..cbdcde4 100644
--- a/tests/unit/test_vcr.py
+++ b/tests/unit/test_vcr.py
@@ -15,9 +15,11 @@ def test_vcr_use_cassette():
         'vcr.cassette.Cassette.load',
         return_value=mock.MagicMock(inject=False)
     ) as mock_cassette_load:
+
         @test_vcr.use_cassette('test')
         def function():
             pass
+
         assert mock_cassette_load.call_count == 0
         function()
         assert mock_cassette_load.call_args[1]['record_mode'] is record_mode
@@ -38,9 +40,11 @@ def test_vcr_use_cassette():
 
 def test_vcr_before_record_request_params():
     base_path = 'http://httpbin.org/'
+
     def before_record_cb(request):
         if request.path != '/get':
             return request
+
     test_vcr = VCR(filter_headers=('cookie',), before_record_request=before_record_cb,
                    ignore_hosts=('www.test.com',), ignore_localhost=True,
                    filter_query_parameters=('foo',))
@@ -53,8 +57,12 @@ def test_vcr_before_record_request_params():
         assert cassette.filter_request(
             Request('GET', base_path + '?foo=bar', '',
                     {'cookie': 'test', 'other': 'fun'})).headers == {'other': 'fun'}
-        assert cassette.filter_request(Request('GET', base_path + '?foo=bar', '',
-                                               {'cookie': 'test', 'other': 'fun'})).headers == {'other': 'fun'}
+        assert cassette.filter_request(
+            Request(
+                'GET', base_path + '?foo=bar', '',
+                {'cookie': 'test', 'other': 'fun'}
+            )
+        ).headers == {'other': 'fun'}
 
         assert cassette.filter_request(Request('GET', 'http://www.test.com' + '?foo=bar', '',
                                                {'cookie': 'test', 'other': 'fun'})) is None
@@ -64,6 +72,32 @@ def test_vcr_before_record_request_params():
         assert cassette.filter_request(Request('GET', base_path + 'get', '', {})) is not None
 
 
+def test_vcr_before_record_response_iterable():
+    # Regression test for #191
+
+    request = Request('GET', '/', '', {})
+    response = object()  # just can't be None
+
+    # Prevent actually saving the cassette
+    with mock.patch('vcr.cassette.save_cassette'):
+
+        # Baseline: non-iterable before_record_response should work
+        mock_filter = mock.Mock()
+        vcr = VCR(before_record_response=mock_filter)
+        with vcr.use_cassette('test') as cassette:
+            assert mock_filter.call_count == 0
+            cassette.append(request, response)
+            assert mock_filter.call_count == 1
+
+        # Regression test: iterable before_record_response should work too
+        mock_filter = mock.Mock()
+        vcr = VCR(before_record_response=(mock_filter,))
+        with vcr.use_cassette('test') as cassette:
+            assert mock_filter.call_count == 0
+            cassette.append(request, response)
+            assert mock_filter.call_count == 1
+
+
 @pytest.fixture
 def random_fixture():
     return 1
@@ -103,6 +137,7 @@ def test_custom_patchers():
 
 def test_inject_cassette():
     vcr = VCR(inject_cassette=True)
+
     @vcr.use_cassette('test', record_mode='once')
     def with_cassette_injected(cassette):
         assert cassette.record_mode == 'once'
@@ -117,9 +152,11 @@ def test_inject_cassette():
 
 def test_with_current_defaults():
     vcr = VCR(inject_cassette=True, record_mode='once')
+
     @vcr.use_cassette('test', with_current_defaults=False)
     def changing_defaults(cassette, checks):
         checks(cassette)
+
     @vcr.use_cassette('test', with_current_defaults=True)
     def current_defaults(cassette, checks):
         checks(cassette)
@@ -141,27 +178,33 @@ def test_with_current_defaults():
 def test_cassette_library_dir_with_decoration_and_no_explicit_path():
     library_dir = '/libary_dir'
     vcr = VCR(inject_cassette=True, cassette_library_dir=library_dir)
+
     @vcr.use_cassette()
     def function_name(cassette):
         assert cassette._path == os.path.join(library_dir, 'function_name')
+
     function_name()
 
 
 def test_cassette_library_dir_with_decoration_and_explicit_path():
     library_dir = '/libary_dir'
     vcr = VCR(inject_cassette=True, cassette_library_dir=library_dir)
+
     @vcr.use_cassette(path='custom_name')
     def function_name(cassette):
         assert cassette._path == os.path.join(library_dir, 'custom_name')
+
     function_name()
 
 
 def test_cassette_library_dir_with_decoration_and_super_explicit_path():
     library_dir = '/libary_dir'
     vcr = VCR(inject_cassette=True, cassette_library_dir=library_dir)
+
     @vcr.use_cassette(path=os.path.join(library_dir, 'custom_name'))
     def function_name(cassette):
         assert cassette._path == os.path.join(library_dir, 'custom_name')
+
     function_name()
 
 
@@ -169,26 +212,32 @@ def test_cassette_library_dir_with_path_transformer():
     library_dir = '/libary_dir'
     vcr = VCR(inject_cassette=True, cassette_library_dir=library_dir,
               path_transformer=lambda path: path + '.json')
+
     @vcr.use_cassette()
     def function_name(cassette):
         assert cassette._path == os.path.join(library_dir, 'function_name.json')
+
     function_name()
 
 
 def test_use_cassette_with_no_extra_invocation():
     vcr = VCR(inject_cassette=True, cassette_library_dir='/')
+
     @vcr.use_cassette
     def function_name(cassette):
         assert cassette._path == os.path.join('/', 'function_name')
+
     function_name()
 
 
 def test_path_transformer():
     vcr = VCR(inject_cassette=True, cassette_library_dir='/',
               path_transformer=lambda x: x + '_test')
+
     @vcr.use_cassette
     def function_name(cassette):
         assert cassette._path == os.path.join('/', 'function_name_test')
+
     function_name()
 
 
@@ -203,8 +252,25 @@ def test_cassette_name_generator_defaults_to_using_module_function_defined_in():
 
 def test_ensure_suffix():
     vcr = VCR(inject_cassette=True, path_transformer=VCR.ensure_suffix('.yaml'))
+
     @vcr.use_cassette
     def function_name(cassette):
         assert cassette._path == os.path.join(os.path.dirname(__file__),
                                               'function_name.yaml')
+
     function_name()
+
+
+def test_additional_matchers():
+    vcr = VCR(match_on=('uri',), inject_cassette=True)
+
+    @vcr.use_cassette
+    def function_defaults(cassette):
+        assert set(cassette._match_on) == set([vcr.matchers['uri']])
+
+    @vcr.use_cassette(additional_matchers=('body',))
+    def function_additional(cassette):
+        assert set(cassette._match_on) == set([vcr.matchers['uri'], vcr.matchers['body']])
+
+    function_defaults()
+    function_additional()
diff --git a/vcr/cassette.py b/vcr/cassette.py
index 87d2598..bc7410f 100644
--- a/vcr/cassette.py
+++ b/vcr/cassette.py
@@ -1,11 +1,9 @@
-"""The container for recorded requests and responses"""
-import functools
+import sys
+import inspect
 import logging
 
-
 import wrapt
 
-# Internal imports
 from .compat import contextlib, collections
 from .errors import UnhandledHTTPRequestError
 from .matchers import requests_match, uri, method
@@ -22,10 +20,18 @@ class CassetteContextDecorator(object):
     """Context manager/decorator that handles installing the cassette and
     removing cassettes.
 
-    This class defers the creation of a new cassette instance until the point at
-    which it is installed by context manager or decorator. The fact that a new
-    cassette is used with each application prevents the state of any cassette
-    from interfering with another.
+    This class defers the creation of a new cassette instance until
+    the point at which it is installed by context manager or
+    decorator. The fact that a new cassette is used with each
+    application prevents the state of any cassette from interfering
+    with another.
+
+    Instances of this class are NOT reentrant as context managers.
+    However, functions that are decorated by
+    ``CassetteContextDecorator`` instances ARE reentrant. See the
+    implementation of ``__call__`` on this class for more details.
+    There is also a guard against attempts to reenter instances of
+    this class as a context manager in ``__exit__``.
     """
 
     _non_cassette_arguments = ('path_transformer', 'func_path_generator')
@@ -43,21 +49,18 @@ class CassetteContextDecorator(object):
         with contextlib.ExitStack() as exit_stack:
             for patcher in CassettePatcherBuilder(cassette).build():
                 exit_stack.enter_context(patcher)
-            log.debug('Entered context for cassette at {0}.'.format(cassette._path))
+            log_format = '{action} context for cassette at {path}.'
+            log.debug(log_format.format(
+                action="Entering", path=cassette._path
+            ))
             yield cassette
-            log.debug('Exiting context for cassette at {0}.'.format(cassette._path))
+            log.debug(log_format.format(
+                action="Exiting", path=cassette._path
+            ))
             # TODO(@IvanMalison): Hmmm. it kind of feels like this should be
             # somewhere else.
             cassette._save()
 
-    @classmethod
-    def key_predicate(cls, key, value):
-        return key in cls._non_cassette_arguments
-
-    @classmethod
-    def _split_keys(cls, kwargs):
-        return partition_dict(cls.key_predicate, kwargs)
-
     def __enter__(self):
         # This assertion is here to prevent the dangerous behavior
         # that would result from forgetting about a __finish before
@@ -68,7 +71,10 @@ class CassetteContextDecorator(object):
         #     with context_decorator:
         #         pass
         assert self.__finish is None, "Cassette already open."
-        other_kwargs, cassette_kwargs = self._split_keys(self._args_getter())
+        other_kwargs, cassette_kwargs = partition_dict(
+            lambda key, _: key in self._non_cassette_arguments,
+            self._args_getter()
+        )
         if 'path_transformer' in other_kwargs:
             transformer = other_kwargs['path_transformer']
             cassette_kwargs['path'] = transformer(cassette_kwargs['path'])
@@ -84,27 +90,55 @@ class CassetteContextDecorator(object):
         # This awkward cloning thing is done to ensure that decorated
         # functions are reentrant. This is required for thread
         # safety and the correct operation of recursive functions.
-        args_getter = self._build_args_getter_for_decorator(
-            function, self._args_getter
+        args_getter = self._build_args_getter_for_decorator(function)
+        return type(self)(self.cls, args_getter)._execute_function(
+            function, args, kwargs
         )
-        clone = type(self)(self.cls, args_getter)
-        with clone as cassette:
-            if cassette.inject:
-                return function(cassette, *args, **kwargs)
-            else:
-                return function(*args, **kwargs)
+
+    def _execute_function(self, function, args, kwargs):
+        if inspect.isgeneratorfunction(function):
+            handler = self._handle_coroutine
+        else:
+            handler = self._handle_function
+        return handler(function, args, kwargs)
+
+    def _handle_coroutine(self, function, args, kwargs):
+        """Wraps a coroutine so that we're inside the cassette context for the
+        duration of the coroutine.
+        """
+        with self as cassette:
+            coroutine = self.__handle_function(cassette, function, args, kwargs)
+            # We don't need to catch StopIteration. The caller (Tornado's
+            # gen.coroutine, for example) will handle that.
+            to_yield = next(coroutine)
+            while True:
+                try:
+                    to_send = yield to_yield
+                except Exception:
+                    to_yield = coroutine.throw(*sys.exc_info())
+                else:
+                    to_yield = coroutine.send(to_send)
+
+    def __handle_function(self, cassette, function, args, kwargs):
+        if cassette.inject:
+            return function(cassette, *args, **kwargs)
+        else:
+            return function(*args, **kwargs)
+
+    def _handle_function(self, function, args, kwargs):
+        with self as cassette:
+            self.__handle_function(cassette, function, args, kwargs)
 
     @staticmethod
     def get_function_name(function):
         return function.__name__
 
-    @classmethod
-    def _build_args_getter_for_decorator(cls, function, args_getter):
+    def _build_args_getter_for_decorator(self, function):
         def new_args_getter():
-            kwargs = args_getter()
+            kwargs = self._args_getter()
             if 'path' not in kwargs:
                 name_generator = (kwargs.get('func_path_generator') or
-                                  cls.get_function_name)
+                                  self.get_function_name)
                 path = name_generator(function)
                 kwargs['path'] = path
             return kwargs
@@ -130,7 +164,7 @@ class Cassette(object):
         return CassetteContextDecorator.from_args(cls, **kwargs)
 
     def __init__(self, path, serializer=yamlserializer, record_mode='once',
-                 match_on=(uri, method),  before_record_request=None,
+                 match_on=(uri, method), before_record_request=None,
                  before_record_response=None, custom_patches=(),
                  inject=False):
 
@@ -176,8 +210,7 @@ class Cassette(object):
         request = self._before_record_request(request)
         if not request:
             return
-        if self._before_record_response:
-            response = self._before_record_response(response)
+        response = self._before_record_response(response)
         self.data.append((request, response))
         self.dirty = True
 
diff --git a/vcr/config.py b/vcr/config.py
index 7655a3a..e2389db 100644
--- a/vcr/config.py
+++ b/vcr/config.py
@@ -67,10 +67,11 @@ class VCR(object):
         try:
             serializer = self.serializers[serializer_name]
         except KeyError:
-            print("Serializer {0} doesn't exist or isn't registered".format(
-                serializer_name
-            ))
-            raise KeyError
+            raise KeyError(
+                "Serializer {0} doesn't exist or isn't registered".format(
+                    serializer_name
+                )
+            )
         return serializer
 
     def _get_matchers(self, matcher_names):
@@ -107,7 +108,7 @@ class VCR(object):
         matcher_names = kwargs.get('match_on', self.match_on)
         path_transformer = kwargs.get(
             'path_transformer',
-            self.path_transformer
+            self.path_transformer or self.ensure_suffix('.yaml')
         )
         func_path_generator = kwargs.get(
             'func_path_generator',
@@ -117,12 +118,16 @@ class VCR(object):
             'cassette_library_dir',
             self.cassette_library_dir
         )
+        additional_matchers = kwargs.get('additional_matchers', ())
+
         if cassette_library_dir:
             def add_cassette_library_dir(path):
                 if not path.startswith(cassette_library_dir):
                     return os.path.join(cassette_library_dir, path)
                 return path
-            path_transformer = compose(add_cassette_library_dir, path_transformer)
+            path_transformer = compose(
+                add_cassette_library_dir, path_transformer
+            )
         elif not func_path_generator:
             # If we don't have a library dir, use the functions
             # location to build a full path for cassettes.
@@ -130,12 +135,12 @@ class VCR(object):
 
         merged_config = {
             'serializer': self._get_serializer(serializer_name),
-            'match_on': self._get_matchers(matcher_names),
+            'match_on': self._get_matchers(
+                tuple(matcher_names) + tuple(additional_matchers)
+            ),
             'record_mode': kwargs.get('record_mode', self.record_mode),
             'before_record_request': self._build_before_record_request(kwargs),
-            'before_record_response': self._build_before_record_response(
-                kwargs
-            ),
+            'before_record_response': self._build_before_record_response(kwargs),
             'custom_patches': self._custom_patches + kwargs.get(
                 'custom_patches', ()
             ),
@@ -153,11 +158,11 @@ class VCR(object):
             'before_record_response', self.before_record_response
         )
         filter_functions = []
-        if before_record_response and not isinstance(before_record_response,
-                                                     collections.Iterable):
-            before_record_response = (before_record_response,)
-            for function in before_record_response:
-                filter_functions.append(function)
+        if before_record_response:
+            if not isinstance(before_record_response, collections.Iterable):
+                before_record_response = (before_record_response,)
+            filter_functions.extend(before_record_response)
... 389 lines suppressed ...

-- 
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/python-modules/packages/vcr.py.git



More information about the Python-modules-commits mailing list