1
0
mirror of https://github.com/kevin1024/vcrpy.git synced 2025-12-09 01:03:24 +00:00

Compare commits

...

24 Commits

Author SHA1 Message Date
Thomas Grainger
0c4020df7d version bump 1.11.0 2017-05-02 11:36:16 +01:00
Thomas Grainger
204cb8f2ac Merge pull request #303 from graingert/support-3.6
support 3.6
2017-05-02 11:17:39 +01:00
Thomas Grainger
0e421b5327 Merge pull request #301 from graingert/handle-pytest-asyncio-coroutines
handle pytest-asyncio async def coroutines
2017-05-02 11:17:06 +01:00
Thomas Grainger
dc2dc306d5 pytest-httpbin doesn't support chunked requests on Python 3.6 2017-04-04 11:30:16 +01:00
Thomas Grainger
1092bcd1a1 add requests 2.13 2017-04-04 10:32:17 +01:00
Thomas Grainger
73dbc6f8cb add missing _get_content_length static method
also add _is_textIO
2017-04-04 10:32:17 +01:00
Thomas Grainger
3e9fb10c11 Merge remote-tracking branch 'derekbekoe/fix-vcrconnection-3.6' into support-3.6 2017-04-04 10:32:17 +01:00
Thomas Grainger
3588ed6341 support 3.6 2017-04-04 10:32:16 +01:00
Kevin McCarthy
26326c3ef0 Merge pull request #305 from graingert/add-cache-to-gitignore
add .cache to gitignore
2017-04-03 06:40:21 -10:00
Thomas Grainger
7514d94262 handle pytest-asyncio async def coroutines 2017-04-03 15:49:49 +01:00
Thomas Grainger
1df577f0fc add .cache to gitignore 2017-04-03 15:45:42 +01:00
Kevin McCarthy
70f4707063 Merge pull request #302 from AartGoossens/feature/before_record_docs_fix
Improves docs for before_record_request
2017-03-16 08:55:34 -10:00
Aart Goossens
521146d64e Improves docs for before_record_request 2017-03-16 19:25:16 +01:00
Derek Bekoe
091b402594 Revert "Add Python 3.6 to CI"
This reverts commit 24b617a427.
2017-01-24 16:26:02 -08:00
Derek Bekoe
24b617a427 Add Python 3.6 to CI 2017-01-24 13:57:37 -08:00
Derek Bekoe
97473bb8d8 Correctly patch HTTPConnection.request in Python 3.6
Fixes https://github.com/kevin1024/vcrpy/issues/293
2017-01-23 14:54:26 -08:00
Kevin McCarthy
ed35643c3e Merge pull request #292 from j-funk/master
Allow injection of persistence methods
2017-01-22 08:29:52 -06:00
Julien Funk
2fb3b52c7e add custom persister docs 2017-01-19 13:10:27 -05:00
Julien Funk
9e70993d57 substiture IOError with more appropriate ValueError 2017-01-19 13:10:08 -05:00
Julien Funk
6887e2cff9 remove unused imports 2017-01-15 15:04:20 -08:00
Julien Funk
ba38680402 Merge pull request #1 from IvanMalison/persistence_methods
Fix patch of FliesystemPersister.load_cassette
2017-01-14 15:59:13 -05:00
Ivan Malison
06b00837fc Fix patch of FliesystemPersister.load_cassette 2017-01-13 15:29:45 -08:00
Julien Funk
a033bc729c refactored, 1 failing test 2017-01-13 16:09:42 -05:00
Julien Funk
6f8486e0a2 allow injection of persistence methods 2017-01-12 16:41:26 -05:00
21 changed files with 251 additions and 79 deletions

1
.gitignore vendored
View File

@@ -1,5 +1,6 @@
*.pyc *.pyc
.tox .tox
.cache
build/ build/
dist/ dist/
*.egg/ *.egg/

View File

@@ -13,6 +13,7 @@ env:
- TOX_SUFFIX="requests25" - TOX_SUFFIX="requests25"
- TOX_SUFFIX="requests26" - TOX_SUFFIX="requests26"
- TOX_SUFFIX="requests27" - TOX_SUFFIX="requests27"
- TOX_SUFFIX="requests213"
- TOX_SUFFIX="requests1" - TOX_SUFFIX="requests1"
- TOX_SUFFIX="httplib2" - TOX_SUFFIX="httplib2"
- TOX_SUFFIX="boto" - TOX_SUFFIX="boto"
@@ -34,10 +35,14 @@ matrix:
python: 3.3 python: 3.3
- env: TOX_SUFFIX="boto" - env: TOX_SUFFIX="boto"
python: 3.4 python: 3.4
- env: TOX_SUFFIX="boto"
python: 3.6
- env: TOX_SUFFIX="requests1" - env: TOX_SUFFIX="requests1"
python: 3.4 python: 3.4
- env: TOX_SUFFIX="requests1" - env: TOX_SUFFIX="requests1"
python: 3.5 python: 3.5
- env: TOX_SUFFIX="requests1"
python: 3.6
- env: TOX_SUFFIX="aiohttp" - env: TOX_SUFFIX="aiohttp"
python: 2.6 python: 2.6
- env: TOX_SUFFIX="aiohttp" - env: TOX_SUFFIX="aiohttp"
@@ -54,6 +59,7 @@ python:
- 3.3 - 3.3
- 3.4 - 3.4
- 3.5 - 3.5
- 3.6
- pypy - pypy
- pypy3 - pypy3
install: install:

View File

@@ -122,6 +122,27 @@ Finally, register your method with VCR to use your new request matcher.
with my_vcr.use_cassette('test.yml'): with my_vcr.use_cassette('test.yml'):
# your http here # your http here
Register your own cassette persister
------------------------------------
Create your own persistence class, see the :ref:`persister_example`.
Your custom persister must implement both ``load_cassette`` and ``save_cassette``
methods. The ``load_cassette`` method must return a deserialized cassette or raise
``ValueError`` if no cassette is found.
Once the persister class is defined, register with VCR like so...
.. code:: python
import vcr
my_vcr = vcr.VCR()
class CustomerPersister(object):
# implement Persister methods...
my_vcr.register_persister(CustomPersister)
Filter sensitive data from the request Filter sensitive data from the request
-------------------------------------- --------------------------------------
@@ -201,7 +222,7 @@ Custom Request filtering
If none of these covers your request filtering needs, you can register a If none of these covers your request filtering needs, you can register a
callback that will manipulate the HTTP request before adding it to the callback that will manipulate the HTTP request before adding it to the
cassette. Use the ``before_record`` configuration option to so this. cassette. Use the ``before_record_request`` configuration option to so this.
Here is an example that will never record requests to the /login Here is an example that will never record requests to the /login
endpoint. endpoint.
@@ -212,7 +233,7 @@ endpoint.
return request return request
my_vcr = vcr.VCR( my_vcr = vcr.VCR(
before_record = before_record_cb, before_record_request = before_record_cb,
) )
with my_vcr.use_cassette('test.yml'): with my_vcr.use_cassette('test.yml'):
# your http code here # your http code here
@@ -229,7 +250,7 @@ path.
return request return request
my_vcr = vcr.VCR( my_vcr = vcr.VCR(
before_record=scrub_login_request, before_record_request=scrub_login_request,
) )
with my_vcr.use_cassette('test.yml'): with my_vcr.use_cassette('test.yml'):
# your http code here # your http code here

View File

@@ -1,5 +1,8 @@
Changelog Changelog
--------- ---------
- 1.11.0 Allow injection of persistence methods + bugfixes (thanks @j-funk and @IvanMalison),
Support python 3.6 + CI tests (thanks @derekbekoe and @graingert),
Support pytest-asyncio coroutines (thanks @graingert)
- 1.10.5 Added a fix to httplib2 (thanks @carlosds730), Fix an issue with - 1.10.5 Added a fix to httplib2 (thanks @carlosds730), Fix an issue with
aiohttp (thanks @madninja), Add missing requirement yarl (thanks @lamenezes), aiohttp (thanks @madninja), Add missing requirement yarl (thanks @lamenezes),
Remove duplicate mock triple (thanks @FooBarQuaxx) Remove duplicate mock triple (thanks @FooBarQuaxx)

View File

@@ -56,7 +56,7 @@ if sys.version_info[0] == 2:
setup( setup(
name='vcrpy', name='vcrpy',
version='1.10.5', version='1.11.0',
description=( description=(
"Automatically mock your HTTP interactions to simplify and " "Automatically mock your HTTP interactions to simplify and "
"speed up testing" "speed up testing"

View File

@@ -1,7 +1,13 @@
import asyncio import asyncio
import aiohttp
@asyncio.coroutine @asyncio.coroutine
def aiohttp_request(session, method, url, as_text, **kwargs): def aiohttp_request(loop, method, url, as_text, **kwargs):
response = yield from session.request(method, url, **kwargs) # NOQA: E999 with aiohttp.ClientSession(loop=loop) as session:
return response, (yield from response.text()) if as_text else (yield from response.json()) # NOQA: E999 response = yield from session.request(method, url, **kwargs) # NOQA: E999
if as_text:
content = yield from response.text() # NOQA: E999
else:
content = yield from response.json() # NOQA: E999
return response, content

View File

@@ -0,0 +1,13 @@
import aiohttp
import pytest
import vcr
@vcr.use_cassette()
@pytest.mark.asyncio
async def test_http(): # noqa: E999
async with aiohttp.ClientSession() as session:
url = 'https://httpbin.org/get'
params = {'ham': 'spam'}
resp = await session.get(url, params=params) # noqa: E999
assert (await resp.json())['args'] == {'ham': 'spam'} # noqa: E999

View File

@@ -1,28 +1,40 @@
import pytest import pytest
aiohttp = pytest.importorskip("aiohttp") aiohttp = pytest.importorskip("aiohttp")
import asyncio # NOQA import asyncio # noqa: E402
import sys # NOQA import contextlib # noqa: E402
import aiohttp # NOQA import pytest # noqa: E402
import pytest # NOQA import vcr # noqa: E402
import vcr # NOQA
from .aiohttp_utils import aiohttp_request # NOQA from .aiohttp_utils import aiohttp_request # noqa: E402
try:
from .async_def import test_http # noqa: F401
except SyntaxError:
pass
def run_in_loop(fn):
with contextlib.closing(asyncio.new_event_loop()) as loop:
asyncio.set_event_loop(loop)
task = loop.create_task(fn(loop))
return loop.run_until_complete(task)
def request(method, url, as_text=True, **kwargs):
def run(loop):
return aiohttp_request(loop, method, url, as_text, **kwargs)
return run_in_loop(run)
def get(url, as_text=True, **kwargs): def get(url, as_text=True, **kwargs):
loop = asyncio.get_event_loop() return request('GET', url, as_text, **kwargs)
with aiohttp.ClientSession() as session:
task = loop.create_task(aiohttp_request(session, 'GET', url, as_text, **kwargs))
return loop.run_until_complete(task)
def post(url, as_text=True, **kwargs): def post(url, as_text=True, **kwargs):
loop = asyncio.get_event_loop() return request('POST', url, as_text, **kwargs)
with aiohttp.ClientSession() as session:
task = loop.create_task(aiohttp_request(session, 'POST', url, as_text, **kwargs))
return loop.run_until_complete(task)
@pytest.fixture(params=["https", "http"]) @pytest.fixture(params=["https", "http"])

View File

@@ -0,0 +1,22 @@
interactions:
- request:
body: null
headers: {}
method: GET
uri: https://httpbin.org/get?ham=spam
response:
body: {string: "{\n \"args\": {\n \"ham\": \"spam\"\n }, \n \"headers\"\
: {\n \"Accept\": \"*/*\", \n \"Accept-Encoding\": \"gzip, deflate\"\
, \n \"Connection\": \"close\", \n \"Host\": \"httpbin.org\", \n \
\ \"User-Agent\": \"Python/3.5 aiohttp/2.0.1\"\n }, \n \"origin\": \"213.86.221.35\"\
, \n \"url\": \"https://httpbin.org/get?ham=spam\"\n}\n"}
headers: {Access-Control-Allow-Credentials: 'true', Access-Control-Allow-Origin: '*',
Connection: keep-alive, Content-Length: '299', Content-Type: application/json,
Date: 'Wed, 22 Mar 2017 20:08:29 GMT', Server: gunicorn/19.7.1, Via: 1.1 vegur}
status: {code: 200, message: OK}
url: !!python/object/new:yarl.URL
state: !!python/tuple
- !!python/object/new:urllib.parse.SplitResult [https, httpbin.org, /get, ham=spam,
'']
- false
version: 1

View File

@@ -0,0 +1,40 @@
# -*- coding: utf-8 -*-
'''Tests for cassettes with custom persistence'''
# External imports
import os
from six.moves.urllib.request import urlopen
# Internal imports
import vcr
from vcr.persisters.filesystem import FilesystemPersister
def test_save_cassette_with_custom_persister(tmpdir, httpbin):
'''Ensure you can save a cassette using custom persister'''
my_vcr = vcr.VCR()
my_vcr.register_persister(FilesystemPersister)
# Check to make sure directory doesnt exist
assert not os.path.exists(str(tmpdir.join('nonexistent')))
# Run VCR to create dir and cassette file using new save_cassette callback
with my_vcr.use_cassette(str(tmpdir.join('nonexistent', 'cassette.yml'))):
urlopen(httpbin.url).read()
# Callback should have made the file and the directory
assert os.path.exists(str(tmpdir.join('nonexistent', 'cassette.yml')))
def test_load_cassette_with_custom_persister(tmpdir, httpbin):
'''
Ensure you can load a cassette using custom persister
'''
my_vcr = vcr.VCR()
my_vcr.register_persister(FilesystemPersister)
test_fixture = str(tmpdir.join('synopsis.json'))
with my_vcr.use_cassette(test_fixture, serializer='json'):
response = urlopen(httpbin.url).read()
assert b'difficult sometimes' in response

View File

@@ -4,8 +4,8 @@ import pytest
import vcr import vcr
from assertions import assert_cassette_empty, assert_is_json from assertions import assert_cassette_empty, assert_is_json
requests = pytest.importorskip("requests") requests = pytest.importorskip("requests")
from requests.exceptions import ConnectionError # noqa E402
def test_status_code(httpbin_both, tmpdir): def test_status_code(httpbin_both, tmpdir):
@@ -100,11 +100,26 @@ def test_post(tmpdir, httpbin_both):
assert req1 == req2 assert req1 == req2
def test_post_chunked_binary(tmpdir, httpbin_both): def test_post_chunked_binary(tmpdir, httpbin):
'''Ensure that we can send chunked binary without breaking while trying to concatenate bytes with str.''' '''Ensure that we can send chunked binary without breaking while trying to concatenate bytes with str.'''
data1 = iter([b'data', b'to', b'send']) data1 = iter([b'data', b'to', b'send'])
data2 = iter([b'data', b'to', b'send']) data2 = iter([b'data', b'to', b'send'])
url = httpbin_both.url + '/post' url = httpbin.url + '/post'
with vcr.use_cassette(str(tmpdir.join('requests.yaml'))):
req1 = requests.post(url, data1).content
with vcr.use_cassette(str(tmpdir.join('requests.yaml'))):
req2 = requests.post(url, data2).content
assert req1 == req2
@pytest.mark.xfail('sys.version_info >= (3, 6)', strict=True, raises=ConnectionError)
def test_post_chunked_binary_secure(tmpdir, httpbin_secure):
'''Ensure that we can send chunked binary without breaking while trying to concatenate bytes with str.'''
data1 = iter([b'data', b'to', b'send'])
data2 = iter([b'data', b'to', b'send'])
url = httpbin_secure.url + '/post'
with vcr.use_cassette(str(tmpdir.join('requests.yaml'))): with vcr.use_cassette(str(tmpdir.join('requests.yaml'))):
req1 = requests.post(url, data1).content req1 = requests.post(url, data1).content
print(req1) print(req1)

View File

@@ -83,7 +83,8 @@ def make_get_request():
@mock.patch('vcr.cassette.requests_match', return_value=True) @mock.patch('vcr.cassette.requests_match', return_value=True)
@mock.patch('vcr.cassette.load_cassette', lambda *args, **kwargs: (('foo',), (mock.MagicMock(),))) @mock.patch('vcr.cassette.FilesystemPersister.load_cassette',
classmethod(lambda *args, **kwargs: (('foo',), (mock.MagicMock(),))))
@mock.patch('vcr.cassette.Cassette.can_play_response_for', return_value=True) @mock.patch('vcr.cassette.Cassette.can_play_response_for', return_value=True)
@mock.patch('vcr.stubs.VCRHTTPResponse') @mock.patch('vcr.stubs.VCRHTTPResponse')
def test_function_decorated_with_use_cassette_can_be_invoked_multiple_times(*args): def test_function_decorated_with_use_cassette_can_be_invoked_multiple_times(*args):

View File

@@ -1,6 +1,6 @@
import pytest import pytest
import vcr.persist from vcr.persisters.filesystem import FilesystemPersister
from vcr.serializers import jsonserializer, yamlserializer from vcr.serializers import jsonserializer, yamlserializer
@@ -10,7 +10,7 @@ from vcr.serializers import jsonserializer, yamlserializer
]) ])
def test_load_cassette_with_old_cassettes(cassette_path, serializer): def test_load_cassette_with_old_cassettes(cassette_path, serializer):
with pytest.raises(ValueError) as excinfo: with pytest.raises(ValueError) as excinfo:
vcr.persist.load_cassette(cassette_path, serializer) FilesystemPersister.load_cassette(cassette_path, serializer)
assert "run the migration script" in excinfo.exconly() assert "run the migration script" in excinfo.exconly()
@@ -20,5 +20,5 @@ def test_load_cassette_with_old_cassettes(cassette_path, serializer):
]) ])
def test_load_cassette_with_invalid_cassettes(cassette_path, serializer): def test_load_cassette_with_invalid_cassettes(cassette_path, serializer):
with pytest.raises(Exception) as excinfo: with pytest.raises(Exception) as excinfo:
vcr.persist.load_cassette(cassette_path, serializer) FilesystemPersister.load_cassette(cassette_path, serializer)
assert "run the migration script" not in excinfo.exconly() assert "run the migration script" not in excinfo.exconly()

View File

@@ -94,7 +94,7 @@ def test_vcr_before_record_response_iterable():
response = object() # just can't be None response = object() # just can't be None
# Prevent actually saving the cassette # Prevent actually saving the cassette
with mock.patch('vcr.cassette.save_cassette'): with mock.patch('vcr.cassette.FilesystemPersister.save_cassette'):
# Baseline: non-iterable before_record_response should work # Baseline: non-iterable before_record_response should work
mock_filter = mock.Mock() mock_filter = mock.Mock()
@@ -118,7 +118,7 @@ def test_before_record_response_as_filter():
response = object() # just can't be None response = object() # just can't be None
# Prevent actually saving the cassette # Prevent actually saving the cassette
with mock.patch('vcr.cassette.save_cassette'): with mock.patch('vcr.cassette.FilesystemPersister.save_cassette'):
filter_all = mock.Mock(return_value=None) filter_all = mock.Mock(return_value=None)
vcr = VCR(before_record_response=filter_all) vcr = VCR(before_record_response=filter_all)
@@ -132,7 +132,7 @@ def test_vcr_path_transformer():
# Regression test for #199 # Regression test for #199
# Prevent actually saving the cassette # Prevent actually saving the cassette
with mock.patch('vcr.cassette.save_cassette'): with mock.patch('vcr.cassette.FilesystemPersister.save_cassette'):
# Baseline: path should be unchanged # Baseline: path should be unchanged
vcr = VCR() vcr = VCR()

16
tox.ini
View File

@@ -1,5 +1,5 @@
[tox] [tox]
envlist = {py26,py27,py33,py34,pypy,pypy3}-{flakes,requests27,requests26,requests25,requests24,requests23,requests22,requests1,httplib2,urllib317,urllib319,urllib3110,tornado3,tornado4,boto,boto3,aiohttp} envlist = {py26,py27,py33,py34,py35,py36,pypy,pypy3}-{flakes,requests213,requests27,requests26,requests25,requests24,requests23,requests22,requests1,httplib2,urllib317,urllib319,urllib3110,tornado3,tornado4,boto,boto3,aiohttp}
[testenv:flakes] [testenv:flakes]
skipsdist = True skipsdist = True
@@ -20,6 +20,7 @@ deps =
pytest-httpbin pytest-httpbin
PyYAML PyYAML
requests1: requests==1.2.3 requests1: requests==1.2.3
requests213: requests==2.13.0
requests27: requests==2.7.0 requests27: requests==2.7.0
requests26: requests==2.6.0 requests26: requests==2.6.0
requests25: requests==2.5.0 requests25: requests==2.5.0
@@ -30,15 +31,16 @@ deps =
urllib317: urllib3==1.7.1 urllib317: urllib3==1.7.1
urllib319: urllib3==1.9.1 urllib319: urllib3==1.9.1
urllib3110: urllib3==1.10.2 urllib3110: urllib3==1.10.2
{py26,py27,py33,py34,pypy}-tornado3: tornado>=3,<4 {py26,py27,py33,py34,py35,py36,pypy}-tornado3: tornado>=3,<4
{py26,py27,py33,py34,pypy}-tornado4: tornado>=4,<5 {py26,py27,py33,py34,py35,py36,pypy}-tornado4: tornado>=4,<5
{py26,py27,py33,py34,pypy}-tornado3: pytest-tornado {py26,py27,py33,py34,py35,py36,pypy}-tornado3: pytest-tornado
{py26,py27,py33,py34,pypy}-tornado4: pytest-tornado {py26,py27,py33,py34,py35,py36,pypy}-tornado4: pytest-tornado
{py26,py27,py33,py34}-tornado3: pycurl {py26,py27,py33,py34,py35,py36}-tornado3: pycurl
{py26,py27,py33,py34}-tornado4: pycurl {py26,py27,py33,py34,py35,py36}-tornado4: pycurl
boto: boto boto: boto
boto3: boto3 boto3: boto3
aiohttp: aiohttp aiohttp: aiohttp
aiohttp: pytest-asyncio
[flake8] [flake8]
max_line_length = 110 max_line_length = 110

7
vcr/_handle_coroutine.py Normal file
View File

@@ -0,0 +1,7 @@
import asyncio
@asyncio.coroutine
def handle_coroutine(vcr, fn):
with vcr as cassette:
return (yield from fn(cassette)) # noqa: E999

View File

@@ -8,10 +8,20 @@ from .compat import contextlib, collections
from .errors import UnhandledHTTPRequestError from .errors import UnhandledHTTPRequestError
from .matchers import requests_match, uri, method from .matchers import requests_match, uri, method
from .patch import CassettePatcherBuilder from .patch import CassettePatcherBuilder
from .persist import load_cassette, save_cassette
from .serializers import yamlserializer from .serializers import yamlserializer
from .persisters.filesystem import FilesystemPersister
from .util import partition_dict from .util import partition_dict
try:
from asyncio import iscoroutinefunction
from ._handle_coroutine import handle_coroutine
except ImportError:
def iscoroutinefunction(*args, **kwargs):
return False
def handle_coroutine(*args, **kwags):
raise NotImplementedError('Not implemented on Python 2')
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@@ -96,18 +106,25 @@ class CassetteContextDecorator(object):
) )
def _execute_function(self, function, args, kwargs): def _execute_function(self, function, args, kwargs):
if inspect.isgeneratorfunction(function): def handle_function(cassette):
handler = self._handle_coroutine if cassette.inject:
else: return function(cassette, *args, **kwargs)
handler = self._handle_function else:
return handler(function, args, kwargs) return function(*args, **kwargs)
def _handle_coroutine(self, function, args, kwargs): if iscoroutinefunction(function):
"""Wraps a coroutine so that we're inside the cassette context for the return handle_coroutine(vcr=self, fn=handle_function)
duration of the coroutine. if inspect.isgeneratorfunction(function):
return self._handle_generator(fn=handle_function)
return self._handle_function(fn=handle_function)
def _handle_generator(self, fn):
"""Wraps a generator so that we're inside the cassette context for the
duration of the generator.
""" """
with self as cassette: with self as cassette:
coroutine = self.__handle_function(cassette, function, args, kwargs) coroutine = fn(cassette)
# We don't need to catch StopIteration. The caller (Tornado's # We don't need to catch StopIteration. The caller (Tornado's
# gen.coroutine, for example) will handle that. # gen.coroutine, for example) will handle that.
to_yield = next(coroutine) to_yield = next(coroutine)
@@ -119,15 +136,9 @@ class CassetteContextDecorator(object):
else: else:
to_yield = coroutine.send(to_send) to_yield = coroutine.send(to_send)
def __handle_function(self, cassette, function, args, kwargs): def _handle_function(self, fn):
if cassette.inject:
return function(cassette, *args, **kwargs)
else:
return function(*args, **kwargs)
def _handle_function(self, function, args, kwargs):
with self as cassette: with self as cassette:
return self.__handle_function(cassette, function, args, kwargs) return fn(cassette)
@staticmethod @staticmethod
def get_function_name(function): def get_function_name(function):
@@ -163,11 +174,11 @@ class Cassette(object):
def use(cls, **kwargs): def use(cls, **kwargs):
return CassetteContextDecorator.from_args(cls, **kwargs) return CassetteContextDecorator.from_args(cls, **kwargs)
def __init__(self, path, serializer=yamlserializer, record_mode='once', def __init__(self, path, serializer=yamlserializer, persister=FilesystemPersister, 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=(), before_record_response=None, custom_patches=(),
inject=False): inject=False):
self._persister = persister
self._path = path self._path = path
self._serializer = serializer self._serializer = serializer
self._match_on = match_on self._match_on = match_on
@@ -271,24 +282,24 @@ class Cassette(object):
def _save(self, force=False): def _save(self, force=False):
if force or self.dirty: if force or self.dirty:
save_cassette( self._persister.save_cassette(
self._path, self._path,
self._as_dict(), self._as_dict(),
serializer=self._serializer serializer=self._serializer,
) )
self.dirty = False self.dirty = False
def _load(self): def _load(self):
try: try:
requests, responses = load_cassette( requests, responses = self._persister.load_cassette(
self._path, self._path,
serializer=self._serializer serializer=self._serializer,
) )
for request, response in zip(requests, responses): for request, response in zip(requests, responses):
self.append(request, response) self.append(request, response)
self.dirty = False self.dirty = False
self.rewound = True self.rewound = True
except IOError: except ValueError:
pass pass
def __str__(self): def __str__(self):

View File

@@ -9,6 +9,7 @@ import six
from .compat import collections from .compat import collections
from .cassette import Cassette from .cassette import Cassette
from .serializers import yamlserializer, jsonserializer from .serializers import yamlserializer, jsonserializer
from .persisters.filesystem import FilesystemPersister
from .util import compose, auto_decorate from .util import compose, auto_decorate
from . import matchers from . import matchers
from . import filters from . import filters
@@ -57,6 +58,7 @@ class VCR(object):
'raw_body': matchers.raw_body, 'raw_body': matchers.raw_body,
'body': matchers.body, 'body': matchers.body,
} }
self.persister = FilesystemPersister
self.record_mode = record_mode self.record_mode = record_mode
self.filter_headers = filter_headers self.filter_headers = filter_headers
self.filter_query_parameters = filter_query_parameters self.filter_query_parameters = filter_query_parameters
@@ -270,6 +272,10 @@ class VCR(object):
def register_matcher(self, name, matcher): def register_matcher(self, name, matcher):
self.matchers[name] = matcher self.matchers[name] = matcher
def register_persister(self, persister):
# Singleton, no name required
self.persister = persister
def test_case(self, predicate=None): def test_case(self, predicate=None):
predicate = predicate or self.is_test_method predicate = predicate or self.is_test_method
return six.with_metaclass(auto_decorate(self.use_cassette, predicate)) return six.with_metaclass(auto_decorate(self.use_cassette, predicate))

View File

@@ -1,14 +0,0 @@
from .persisters.filesystem import FilesystemPersister
from .serialize import serialize, deserialize
def load_cassette(cassette_path, serializer):
with open(cassette_path) as f:
cassette_content = f.read()
cassette = deserialize(cassette_content, serializer)
return cassette
def save_cassette(cassette_path, cassette_dict, serializer):
data = serialize(cassette_dict, serializer)
FilesystemPersister.write(cassette_path, data)

View File

@@ -1,9 +1,24 @@
# .. _persister_example:
import os import os
from ..serialize import serialize, deserialize
class FilesystemPersister(object): class FilesystemPersister(object):
@classmethod @classmethod
def write(cls, cassette_path, data): def load_cassette(cls, cassette_path, serializer):
try:
with open(cassette_path) as f:
cassette_content = f.read()
except IOError:
raise ValueError('Cassette not found.')
cassette = deserialize(cassette_content, serializer)
return cassette
@staticmethod
def save_cassette(cassette_path, cassette_dict, serializer):
data = serialize(cassette_dict, serializer)
dirname, filename = os.path.split(cassette_path) dirname, filename = os.path.split(cassette_path)
if dirname and not os.path.exists(dirname): if dirname and not os.path.exists(dirname):
os.makedirs(dirname) os.makedirs(dirname)

View File

@@ -153,7 +153,7 @@ class VCRConnection(object):
) )
return uri.replace(prefix, '', 1) return uri.replace(prefix, '', 1)
def request(self, method, url, body=None, headers=None): def request(self, method, url, body=None, headers=None, *args, **kwargs):
'''Persist the request metadata in self._vcr_request''' '''Persist the request metadata in self._vcr_request'''
self._vcr_request = Request( self._vcr_request = Request(
method=method, method=method,
@@ -335,6 +335,11 @@ class VCRConnection(object):
super(VCRConnection, self).__setattr__(name, value) super(VCRConnection, self).__setattr__(name, value)
for k, v in HTTPConnection.__dict__.items():
if isinstance(v, staticmethod):
setattr(VCRConnection, k, v)
class VCRHTTPConnection(VCRConnection): class VCRHTTPConnection(VCRConnection):
'''A Mocked class for HTTP requests''' '''A Mocked class for HTTP requests'''
_baseclass = HTTPConnection _baseclass = HTTPConnection