[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