mirror of
https://github.com/kevin1024/vcrpy.git
synced 2025-12-09 17:15:35 +00:00
Compare commits
69 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0c4020df7d | ||
|
|
204cb8f2ac | ||
|
|
0e421b5327 | ||
|
|
dc2dc306d5 | ||
|
|
1092bcd1a1 | ||
|
|
73dbc6f8cb | ||
|
|
3e9fb10c11 | ||
|
|
3588ed6341 | ||
|
|
26326c3ef0 | ||
|
|
7514d94262 | ||
|
|
1df577f0fc | ||
|
|
70f4707063 | ||
|
|
521146d64e | ||
|
|
091b402594 | ||
|
|
24b617a427 | ||
|
|
97473bb8d8 | ||
|
|
ed35643c3e | ||
|
|
2fb3b52c7e | ||
|
|
9e70993d57 | ||
|
|
6887e2cff9 | ||
|
|
ba38680402 | ||
|
|
06b00837fc | ||
|
|
a033bc729c | ||
|
|
6f8486e0a2 | ||
|
|
53c55b13e7 | ||
|
|
365e7cb112 | ||
|
|
e5d6327de9 | ||
|
|
d86ffe7130 | ||
|
|
d9fd563812 | ||
|
|
9e548718e5 | ||
|
|
83720793fb | ||
|
|
188326b10e | ||
|
|
ff90190660 | ||
|
|
1d9f8b5f7c | ||
|
|
2454aa2eb0 | ||
|
|
df5f6089af | ||
|
|
5738547288 | ||
|
|
8274b660c6 | ||
|
|
a8f1a65d62 | ||
|
|
9c275dd86a | ||
|
|
1fbd65a702 | ||
|
|
31b0e825b5 | ||
|
|
973d8339b3 | ||
|
|
c8db6cb731 | ||
|
|
ecbc192fc4 | ||
|
|
76d365314a | ||
|
|
830a3c2e04 | ||
|
|
9c432c7e50 | ||
|
|
6f7f45d0a8 | ||
|
|
8e352feb6a | ||
|
|
57a934d14b | ||
|
|
f9d7ccd33e | ||
|
|
265a158fe7 | ||
|
|
c65ff0e7b3 | ||
|
|
066752aa0b | ||
|
|
9a5214888b | ||
|
|
609d8e35be | ||
|
|
ce14de8251 | ||
|
|
574b22a62a | ||
|
|
1167b9ea4e | ||
|
|
77ae99bfda | ||
|
|
8851571ba7 | ||
|
|
f71d28d10e | ||
|
|
3355bd01eb | ||
|
|
17afa82bf4 | ||
|
|
f98684e8aa | ||
|
|
5a85e88a39 | ||
|
|
d2368eb2c4 | ||
|
|
37665581e0 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,5 +1,6 @@
|
|||||||
*.pyc
|
*.pyc
|
||||||
.tox
|
.tox
|
||||||
|
.cache
|
||||||
build/
|
build/
|
||||||
dist/
|
dist/
|
||||||
*.egg/
|
*.egg/
|
||||||
|
|||||||
19
.travis.yml
19
.travis.yml
@@ -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"
|
||||||
@@ -22,25 +23,43 @@ env:
|
|||||||
- TOX_SUFFIX="urllib3110"
|
- TOX_SUFFIX="urllib3110"
|
||||||
- TOX_SUFFIX="tornado3"
|
- TOX_SUFFIX="tornado3"
|
||||||
- TOX_SUFFIX="tornado4"
|
- TOX_SUFFIX="tornado4"
|
||||||
|
- TOX_SUFFIX="aiohttp"
|
||||||
matrix:
|
matrix:
|
||||||
allow_failures:
|
allow_failures:
|
||||||
- env: TOX_SUFFIX="boto"
|
- env: TOX_SUFFIX="boto"
|
||||||
- env: TOX_SUFFIX="boto3"
|
- env: TOX_SUFFIX="boto3"
|
||||||
exclude:
|
exclude:
|
||||||
|
- env: TOX_SUFFIX="flakes"
|
||||||
|
python: 2.6
|
||||||
- env: TOX_SUFFIX="boto"
|
- env: TOX_SUFFIX="boto"
|
||||||
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"
|
||||||
|
python: 2.6
|
||||||
|
- env: TOX_SUFFIX="aiohttp"
|
||||||
|
python: 2.7
|
||||||
|
- env: TOX_SUFFIX="aiohttp"
|
||||||
|
python: 3.3
|
||||||
|
- env: TOX_SUFFIX="aiohttp"
|
||||||
|
python: pypy
|
||||||
|
- env: TOX_SUFFIX="aiohttp"
|
||||||
|
python: pypy3
|
||||||
python:
|
python:
|
||||||
- 2.6
|
- 2.6
|
||||||
- 2.7
|
- 2.7
|
||||||
- 3.3
|
- 3.3
|
||||||
- 3.4
|
- 3.4
|
||||||
- 3.5
|
- 3.5
|
||||||
|
- 3.6
|
||||||
- pypy
|
- pypy
|
||||||
- pypy3
|
- pypy3
|
||||||
install:
|
install:
|
||||||
|
|||||||
16
README.rst
16
README.rst
@@ -13,7 +13,7 @@ Source code
|
|||||||
https://github.com/kevin1024/vcrpy
|
https://github.com/kevin1024/vcrpy
|
||||||
|
|
||||||
Documentation
|
Documentation
|
||||||
https://vcrpy.readthedocs.org/
|
https://vcrpy.readthedocs.io/
|
||||||
|
|
||||||
Rationale
|
Rationale
|
||||||
---------
|
---------
|
||||||
@@ -41,6 +41,20 @@ VCR.py will detect the absence of a cassette file and once again record
|
|||||||
all HTTP interactions, which will update them to correspond to the new
|
all HTTP interactions, which will update them to correspond to the new
|
||||||
API.
|
API.
|
||||||
|
|
||||||
|
Support
|
||||||
|
-------
|
||||||
|
|
||||||
|
VCR.py works great with the following HTTP clients:
|
||||||
|
|
||||||
|
- requests
|
||||||
|
- aiohttp
|
||||||
|
- urllib3
|
||||||
|
- tornado
|
||||||
|
- urllib2
|
||||||
|
- boto
|
||||||
|
- boto3
|
||||||
|
|
||||||
|
|
||||||
License
|
License
|
||||||
=======
|
=======
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -1,5 +1,18 @@
|
|||||||
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
|
||||||
|
aiohttp (thanks @madninja), Add missing requirement yarl (thanks @lamenezes),
|
||||||
|
Remove duplicate mock triple (thanks @FooBarQuaxx)
|
||||||
|
- 1.10.4 Fix an issue with asyncio aiohttp (thanks @madninja)
|
||||||
|
- 1.10.3 Fix some issues with asyncio and params (thanks @anovikov1984 and
|
||||||
|
@lamenezes), Fix some issues with cassette serialize / deserialize and empty
|
||||||
|
response bodies (thanks @gRoussac and @dz0ny)
|
||||||
|
- 1.10.2 Fix 1.10.1 release - add aiohttp support back in
|
||||||
|
- 1.10.1 [bad release] Fix build for Fedora package + python2 (thanks @puiterwijk and @lamenezes)
|
||||||
|
- 1.10.0 Add support for aiohttp (thanks @lamenezes)
|
||||||
- 1.9.0 Add support for boto3 (thanks @desdm, @foorbarna). Fix deepcopy issue
|
- 1.9.0 Add support for boto3 (thanks @desdm, @foorbarna). Fix deepcopy issue
|
||||||
for response headers when `decode_compressed_response` is enabled (thanks
|
for response headers when `decode_compressed_response` is enabled (thanks
|
||||||
@nickdirienzo)
|
@nickdirienzo)
|
||||||
|
|||||||
@@ -110,7 +110,7 @@ todo_include_todos = False
|
|||||||
# -- Options for HTML output ----------------------------------------------
|
# -- Options for HTML output ----------------------------------------------
|
||||||
|
|
||||||
# The theme to use for HTML and HTML Help pages.
|
# The theme to use for HTML and HTML Help pages.
|
||||||
# https://read-the-docs.readthedocs.org/en/latest/theme.html#how-do-i-use-this-locally-and-on-read-the-docs
|
# https://read-the-docs.readthedocs.io/en/latest/theme.html#how-do-i-use-this-locally-and-on-read-the-docs
|
||||||
if 'READTHEDOCS' not in os.environ:
|
if 'READTHEDOCS' not in os.environ:
|
||||||
import sphinx_rtd_theme
|
import sphinx_rtd_theme
|
||||||
html_theme = 'sphinx_rtd_theme'
|
html_theme = 'sphinx_rtd_theme'
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ that has ``requests`` installed.
|
|||||||
|
|
||||||
Also, in order for the boto tests to run, you will need an AWS key.
|
Also, in order for the boto tests to run, you will need an AWS key.
|
||||||
Refer to the `boto
|
Refer to the `boto
|
||||||
documentation <http://boto.readthedocs.org/en/latest/getting_started.html>`__
|
documentation <https://boto.readthedocs.io/en/latest/getting_started.html>`__
|
||||||
for how to set this up. I have marked the boto tests as optional in
|
for how to set this up. I have marked the boto tests as optional in
|
||||||
Travis so you don't have to worry about them failing if you submit a
|
Travis so you don't have to worry about them failing if you submit a
|
||||||
pull request.
|
pull request.
|
||||||
|
|||||||
9
setup.py
9
setup.py
@@ -31,6 +31,7 @@ extras_require = {
|
|||||||
':python_version in "2.4, 2.5, 2.6"':
|
':python_version in "2.4, 2.5, 2.6"':
|
||||||
['contextlib2', 'backport_collections', 'mock'],
|
['contextlib2', 'backport_collections', 'mock'],
|
||||||
':python_version in "2.7, 3.1, 3.2"': ['contextlib2', 'mock'],
|
':python_version in "2.7, 3.1, 3.2"': ['contextlib2', 'mock'],
|
||||||
|
':python_version in "3.4, 3.5, 3.6"': ['yarl'],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -49,9 +50,13 @@ except Exception:
|
|||||||
install_requires.extend(value)
|
install_requires.extend(value)
|
||||||
|
|
||||||
|
|
||||||
|
excluded_packages = ["tests*"]
|
||||||
|
if sys.version_info[0] == 2:
|
||||||
|
excluded_packages.append("vcr.stubs.aiohttp_stubs")
|
||||||
|
|
||||||
setup(
|
setup(
|
||||||
name='vcrpy',
|
name='vcrpy',
|
||||||
version='1.9.0',
|
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"
|
||||||
@@ -60,7 +65,7 @@ setup(
|
|||||||
author='Kevin McCarthy',
|
author='Kevin McCarthy',
|
||||||
author_email='me@kevinmccarthy.org',
|
author_email='me@kevinmccarthy.org',
|
||||||
url='https://github.com/kevin1024/vcrpy',
|
url='https://github.com/kevin1024/vcrpy',
|
||||||
packages=find_packages(exclude=("tests*",)),
|
packages=find_packages(exclude=excluded_packages),
|
||||||
install_requires=install_requires,
|
install_requires=install_requires,
|
||||||
extras_require=extras_require,
|
extras_require=extras_require,
|
||||||
license='MIT',
|
license='MIT',
|
||||||
|
|||||||
13
tests/integration/aiohttp_utils.py
Normal file
13
tests/integration/aiohttp_utils.py
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import asyncio
|
||||||
|
import aiohttp
|
||||||
|
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
|
def aiohttp_request(loop, method, url, as_text, **kwargs):
|
||||||
|
with aiohttp.ClientSession(loop=loop) as session:
|
||||||
|
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
|
||||||
13
tests/integration/async_def.py
Normal file
13
tests/integration/async_def.py
Normal 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
|
||||||
129
tests/integration/test_aiohttp.py
Normal file
129
tests/integration/test_aiohttp.py
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
import pytest
|
||||||
|
aiohttp = pytest.importorskip("aiohttp")
|
||||||
|
|
||||||
|
import asyncio # noqa: E402
|
||||||
|
import contextlib # noqa: E402
|
||||||
|
|
||||||
|
import pytest # noqa: E402
|
||||||
|
import vcr # noqa: E402
|
||||||
|
|
||||||
|
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):
|
||||||
|
return request('GET', url, as_text, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
def post(url, as_text=True, **kwargs):
|
||||||
|
return request('POST', url, as_text, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(params=["https", "http"])
|
||||||
|
def scheme(request):
|
||||||
|
'''Fixture that returns both http and https.'''
|
||||||
|
return request.param
|
||||||
|
|
||||||
|
|
||||||
|
def test_status(tmpdir, scheme):
|
||||||
|
url = scheme + '://httpbin.org'
|
||||||
|
with vcr.use_cassette(str(tmpdir.join('status.yaml'))):
|
||||||
|
response, _ = get(url)
|
||||||
|
|
||||||
|
with vcr.use_cassette(str(tmpdir.join('status.yaml'))) as cassette:
|
||||||
|
cassette_response, _ = get(url)
|
||||||
|
assert cassette_response.status == response.status
|
||||||
|
assert cassette.play_count == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_headers(tmpdir, scheme):
|
||||||
|
url = scheme + '://httpbin.org'
|
||||||
|
with vcr.use_cassette(str(tmpdir.join('headers.yaml'))):
|
||||||
|
response, _ = get(url)
|
||||||
|
|
||||||
|
with vcr.use_cassette(str(tmpdir.join('headers.yaml'))) as cassette:
|
||||||
|
cassette_response, _ = get(url)
|
||||||
|
assert cassette_response.headers == response.headers
|
||||||
|
assert cassette.play_count == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_text(tmpdir, scheme):
|
||||||
|
url = scheme + '://httpbin.org'
|
||||||
|
with vcr.use_cassette(str(tmpdir.join('text.yaml'))):
|
||||||
|
_, response_text = get(url)
|
||||||
|
|
||||||
|
with vcr.use_cassette(str(tmpdir.join('text.yaml'))) as cassette:
|
||||||
|
_, cassette_response_text = get(url)
|
||||||
|
assert cassette_response_text == response_text
|
||||||
|
assert cassette.play_count == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_json(tmpdir, scheme):
|
||||||
|
url = scheme + '://httpbin.org/get'
|
||||||
|
with vcr.use_cassette(str(tmpdir.join('json.yaml'))):
|
||||||
|
_, response_json = get(url, as_text=False)
|
||||||
|
|
||||||
|
with vcr.use_cassette(str(tmpdir.join('json.yaml'))) as cassette:
|
||||||
|
_, cassette_response_json = get(url, as_text=False)
|
||||||
|
assert cassette_response_json == response_json
|
||||||
|
assert cassette.play_count == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_post(tmpdir, scheme):
|
||||||
|
data = {'key1': 'value1', 'key2': 'value2'}
|
||||||
|
url = scheme + '://httpbin.org/post'
|
||||||
|
with vcr.use_cassette(str(tmpdir.join('post.yaml'))):
|
||||||
|
_, response_json = post(url, data=data)
|
||||||
|
|
||||||
|
with vcr.use_cassette(str(tmpdir.join('post.yaml'))) as cassette:
|
||||||
|
_, cassette_response_json = post(url, data=data)
|
||||||
|
assert cassette_response_json == response_json
|
||||||
|
assert cassette.play_count == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_params(tmpdir, scheme):
|
||||||
|
url = scheme + '://httpbin.org/get'
|
||||||
|
params = {'a': 1, 'b': False, 'c': 'c'}
|
||||||
|
with vcr.use_cassette(str(tmpdir.join('get.yaml'))) as cassette:
|
||||||
|
_, response_json = get(url, as_text=False, params=params)
|
||||||
|
|
||||||
|
with vcr.use_cassette(str(tmpdir.join('get.yaml'))) as cassette:
|
||||||
|
_, cassette_response_json = get(url, as_text=False, params=params)
|
||||||
|
assert cassette_response_json == response_json
|
||||||
|
assert cassette.play_count == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_params_same_url_distinct_params(tmpdir, scheme):
|
||||||
|
url = scheme + '://httpbin.org/get'
|
||||||
|
params = {'a': 1, 'b': False, 'c': 'c'}
|
||||||
|
with vcr.use_cassette(str(tmpdir.join('get.yaml'))) as cassette:
|
||||||
|
_, response_json = get(url, as_text=False, params=params)
|
||||||
|
|
||||||
|
with vcr.use_cassette(str(tmpdir.join('get.yaml'))) as cassette:
|
||||||
|
_, cassette_response_json = get(url, as_text=False, params=params)
|
||||||
|
assert cassette_response_json == response_json
|
||||||
|
assert cassette.play_count == 1
|
||||||
|
|
||||||
|
other_params = {'other': 'params'}
|
||||||
|
with vcr.use_cassette(str(tmpdir.join('get.yaml'))) as cassette:
|
||||||
|
response, cassette_response_text = get(url, as_text=True, params=other_params)
|
||||||
|
assert 'No match for the request' in cassette_response_text
|
||||||
|
assert response.status == 599
|
||||||
22
tests/integration/test_http
Normal file
22
tests/integration/test_http
Normal 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
|
||||||
40
tests/integration/test_register_persister.py
Normal file
40
tests/integration/test_register_persister.py
Normal 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
|
||||||
@@ -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):
|
||||||
@@ -38,6 +38,18 @@ def test_body(tmpdir, httpbin_both):
|
|||||||
assert content == requests.get(url).content
|
assert content == requests.get(url).content
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_empty_content_type_json(tmpdir, httpbin_both):
|
||||||
|
'''Ensure GET with application/json content-type and empty request body doesn't crash'''
|
||||||
|
url = httpbin_both + '/status/200'
|
||||||
|
headers = {'Content-Type': 'application/json'}
|
||||||
|
|
||||||
|
with vcr.use_cassette(str(tmpdir.join('get_empty_json.yaml')), match_on=('body',)):
|
||||||
|
status = requests.get(url, headers=headers).status_code
|
||||||
|
|
||||||
|
with vcr.use_cassette(str(tmpdir.join('get_empty_json.yaml')), match_on=('body',)):
|
||||||
|
assert status == requests.get(url, headers=headers).status_code
|
||||||
|
|
||||||
|
|
||||||
def test_effective_url(tmpdir, httpbin_both):
|
def test_effective_url(tmpdir, httpbin_both):
|
||||||
'''Ensure that the effective_url is captured'''
|
'''Ensure that the effective_url is captured'''
|
||||||
url = httpbin_both.url + '/redirect-to?url=/html'
|
url = httpbin_both.url + '/redirect-to?url=/html'
|
||||||
@@ -88,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)
|
||||||
|
|||||||
@@ -67,8 +67,8 @@ def test_original_decoded_response_is_not_modified(tmpdir, httpbin):
|
|||||||
assert 'gzip' == inside.headers['content-encoding']
|
assert 'gzip' == inside.headers['content-encoding']
|
||||||
|
|
||||||
# They should effectively be the same response.
|
# They should effectively be the same response.
|
||||||
inside_headers = (h for h in inside.headers.items() if h[0] != 'Date')
|
inside_headers = (h for h in inside.headers.items() if h[0].lower() != 'date')
|
||||||
outside_headers = (h for h in outside.getheaders() if h[0] != 'Date')
|
outside_headers = (h for h in outside.getheaders() if h[0].lower() != 'date')
|
||||||
assert set(inside_headers) == set(outside_headers)
|
assert set(inside_headers) == set(outside_headers)
|
||||||
inside = zlib.decompress(inside.read(), 16+zlib.MAX_WBITS)
|
inside = zlib.decompress(inside.read(), 16+zlib.MAX_WBITS)
|
||||||
outside = zlib.decompress(outside.read(), 16+zlib.MAX_WBITS)
|
outside = zlib.decompress(outside.read(), 16+zlib.MAX_WBITS)
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import pytest
|
|||||||
from vcr.compat import mock
|
from vcr.compat import mock
|
||||||
from vcr.request import Request
|
from vcr.request import Request
|
||||||
from vcr.serialize import deserialize, serialize
|
from vcr.serialize import deserialize, serialize
|
||||||
from vcr.serializers import yamlserializer, jsonserializer
|
from vcr.serializers import yamlserializer, jsonserializer, compat
|
||||||
|
|
||||||
|
|
||||||
def test_deserialize_old_yaml_cassette():
|
def test_deserialize_old_yaml_cassette():
|
||||||
@@ -131,3 +131,9 @@ def test_serialize_binary_request():
|
|||||||
)
|
)
|
||||||
except (UnicodeDecodeError, TypeError) as exc:
|
except (UnicodeDecodeError, TypeError) as exc:
|
||||||
assert msg in str(exc)
|
assert msg in str(exc)
|
||||||
|
|
||||||
|
|
||||||
|
def test_deserialize_no_body_string():
|
||||||
|
data = {'body': {'string': None}}
|
||||||
|
output = compat.convert_to_bytes(data)
|
||||||
|
assert data == output
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
from vcr.stubs import VCRHTTPSConnection
|
from vcr.stubs import VCRHTTPSConnection
|
||||||
|
from vcr.compat import mock
|
||||||
|
from vcr.cassette import Cassette
|
||||||
|
|
||||||
|
|
||||||
class TestVCRConnection(object):
|
class TestVCRConnection(object):
|
||||||
@@ -7,3 +9,10 @@ class TestVCRConnection(object):
|
|||||||
vcr_connection = VCRHTTPSConnection('www.examplehost.com')
|
vcr_connection = VCRHTTPSConnection('www.examplehost.com')
|
||||||
vcr_connection.ssl_version = 'example_ssl_version'
|
vcr_connection.ssl_version = 'example_ssl_version'
|
||||||
assert vcr_connection.real_connection.ssl_version == 'example_ssl_version'
|
assert vcr_connection.real_connection.ssl_version == 'example_ssl_version'
|
||||||
|
|
||||||
|
@mock.patch('vcr.cassette.Cassette.can_play_response_for', return_value=False)
|
||||||
|
def testing_connect(*args):
|
||||||
|
vcr_connection = VCRHTTPSConnection('www.google.com')
|
||||||
|
vcr_connection.cassette = Cassette('test', record_mode='all')
|
||||||
|
vcr_connection.real_connection.connect()
|
||||||
|
assert vcr_connection.real_connection.sock is not None
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
19
tox.ini
19
tox.ini
@@ -1,11 +1,11 @@
|
|||||||
[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}
|
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
|
||||||
commands =
|
commands =
|
||||||
flake8 --version
|
flake8 --version
|
||||||
flake8 --exclude="./docs/conf.py"
|
flake8 --exclude=./docs/conf.py,./.tox/
|
||||||
pyflakes ./docs/conf.py
|
pyflakes ./docs/conf.py
|
||||||
deps = flake8
|
deps = flake8
|
||||||
|
|
||||||
@@ -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,14 +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: pytest-asyncio
|
||||||
|
|
||||||
[flake8]
|
[flake8]
|
||||||
max_line_length = 110
|
max_line_length = 110
|
||||||
|
|||||||
7
vcr/_handle_coroutine.py
Normal file
7
vcr/_handle_coroutine.py
Normal 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
|
||||||
@@ -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):
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|||||||
@@ -49,7 +49,8 @@ def _transform_json(body):
|
|||||||
# Request body is always a byte string, but json.loads() wants a text
|
# Request body is always a byte string, but json.loads() wants a text
|
||||||
# string. RFC 7159 says the default encoding is UTF-8 (although UTF-16
|
# string. RFC 7159 says the default encoding is UTF-8 (although UTF-16
|
||||||
# and UTF-32 are also allowed: hmmmmm).
|
# and UTF-32 are also allowed: hmmmmm).
|
||||||
return json.loads(body.decode('utf-8'))
|
if body:
|
||||||
|
return json.loads(body.decode('utf-8'))
|
||||||
|
|
||||||
|
|
||||||
_xml_header_checker = _header_checker('text/xml')
|
_xml_header_checker = _header_checker('text/xml')
|
||||||
|
|||||||
@@ -164,5 +164,6 @@ def main():
|
|||||||
sys.stderr.write("[{0}] {1}\n".format(status, file_path))
|
sys.stderr.write("[{0}] {1}\n".format(status, file_path))
|
||||||
sys.stderr.write("Done.\n")
|
sys.stderr.write("Done.\n")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
main()
|
main()
|
||||||
|
|||||||
23
vcr/patch.py
23
vcr/patch.py
@@ -80,6 +80,13 @@ else:
|
|||||||
_CurlAsyncHTTPClient_fetch_impl = \
|
_CurlAsyncHTTPClient_fetch_impl = \
|
||||||
tornado.curl_httpclient.CurlAsyncHTTPClient.fetch_impl
|
tornado.curl_httpclient.CurlAsyncHTTPClient.fetch_impl
|
||||||
|
|
||||||
|
try:
|
||||||
|
import aiohttp.client
|
||||||
|
except ImportError: # pragma: no cover
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
_AiohttpClientSessionRequest = aiohttp.client.ClientSession._request
|
||||||
|
|
||||||
|
|
||||||
class CassettePatcherBuilder(object):
|
class CassettePatcherBuilder(object):
|
||||||
|
|
||||||
@@ -98,7 +105,7 @@ class CassettePatcherBuilder(object):
|
|||||||
def build(self):
|
def build(self):
|
||||||
return itertools.chain(
|
return itertools.chain(
|
||||||
self._httplib(), self._requests(), self._boto3(), self._urllib3(),
|
self._httplib(), self._requests(), self._boto3(), self._urllib3(),
|
||||||
self._httplib2(), self._boto(), self._tornado(),
|
self._httplib2(), self._boto(), self._tornado(), self._aiohttp(),
|
||||||
self._build_patchers_from_mock_triples(
|
self._build_patchers_from_mock_triples(
|
||||||
self._cassette.custom_patches
|
self._cassette.custom_patches
|
||||||
),
|
),
|
||||||
@@ -273,6 +280,19 @@ class CassettePatcherBuilder(object):
|
|||||||
)
|
)
|
||||||
yield curl.CurlAsyncHTTPClient, 'fetch_impl', new_fetch_impl
|
yield curl.CurlAsyncHTTPClient, 'fetch_impl', new_fetch_impl
|
||||||
|
|
||||||
|
@_build_patchers_from_mock_triples_decorator
|
||||||
|
def _aiohttp(self):
|
||||||
|
try:
|
||||||
|
import aiohttp.client as client
|
||||||
|
except ImportError: # pragma: no cover
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
from .stubs.aiohttp_stubs import vcr_request
|
||||||
|
new_request = vcr_request(
|
||||||
|
self._cassette, _AiohttpClientSessionRequest
|
||||||
|
)
|
||||||
|
yield client.ClientSession, '_request', new_request
|
||||||
|
|
||||||
def _urllib3_patchers(self, cpool, stubs):
|
def _urllib3_patchers(self, cpool, stubs):
|
||||||
http_connection_remover = ConnectionRemover(
|
http_connection_remover = ConnectionRemover(
|
||||||
self._get_cassette_subclass(stubs.VCRRequestsHTTPConnection)
|
self._get_cassette_subclass(stubs.VCRRequestsHTTPConnection)
|
||||||
@@ -281,7 +301,6 @@ class CassettePatcherBuilder(object):
|
|||||||
self._get_cassette_subclass(stubs.VCRRequestsHTTPSConnection)
|
self._get_cassette_subclass(stubs.VCRRequestsHTTPSConnection)
|
||||||
)
|
)
|
||||||
mock_triples = (
|
mock_triples = (
|
||||||
(cpool, 'VerifiedHTTPSConnection', stubs.VCRRequestsHTTPSConnection),
|
|
||||||
(cpool, 'VerifiedHTTPSConnection', stubs.VCRRequestsHTTPSConnection),
|
(cpool, 'VerifiedHTTPSConnection', stubs.VCRRequestsHTTPSConnection),
|
||||||
(cpool, 'HTTPConnection', stubs.VCRRequestsHTTPConnection),
|
(cpool, 'HTTPConnection', stubs.VCRRequestsHTTPConnection),
|
||||||
(cpool, 'HTTPSConnection', stubs.VCRRequestsHTTPSConnection),
|
(cpool, 'HTTPSConnection', stubs.VCRRequestsHTTPSConnection),
|
||||||
|
|||||||
@@ -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)
|
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ def convert_body_to_bytes(resp):
|
|||||||
http://pyyaml.org/wiki/PyYAMLDocumentation#Python3support
|
http://pyyaml.org/wiki/PyYAMLDocumentation#Python3support
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
if not isinstance(resp['body']['string'], six.binary_type):
|
if resp['body']['string'] is not None and not isinstance(resp['body']['string'], six.binary_type):
|
||||||
resp['body']['string'] = resp['body']['string'].encode('utf-8')
|
resp['body']['string'] = resp['body']['string'].encode('utf-8')
|
||||||
except (KeyError, TypeError, UnicodeEncodeError):
|
except (KeyError, TypeError, UnicodeEncodeError):
|
||||||
# The thing we were converting either wasn't a dictionary or didn't
|
# The thing we were converting either wasn't a dictionary or didn't
|
||||||
|
|||||||
@@ -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,
|
||||||
@@ -287,7 +287,9 @@ class VCRConnection(object):
|
|||||||
# Cassette is write-protected, don't actually connect
|
# Cassette is write-protected, don't actually connect
|
||||||
return
|
return
|
||||||
|
|
||||||
return self.real_connection.connect(*args, **kwargs)
|
from vcr.patch import force_reset
|
||||||
|
with force_reset():
|
||||||
|
return self.real_connection.connect(*args, **kwargs)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def sock(self):
|
def sock(self):
|
||||||
@@ -333,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
|
||||||
|
|||||||
81
vcr/stubs/aiohttp_stubs/__init__.py
Normal file
81
vcr/stubs/aiohttp_stubs/__init__.py
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
'''Stubs for aiohttp HTTP clients'''
|
||||||
|
from __future__ import absolute_import
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import functools
|
||||||
|
import json
|
||||||
|
|
||||||
|
from aiohttp import ClientResponse
|
||||||
|
from yarl import URL
|
||||||
|
|
||||||
|
from vcr.request import Request
|
||||||
|
|
||||||
|
|
||||||
|
class MockClientResponse(ClientResponse):
|
||||||
|
# TODO: get encoding from header
|
||||||
|
@asyncio.coroutine
|
||||||
|
def json(self, *, encoding='utf-8', loads=json.loads): # NOQA: E999
|
||||||
|
return loads(self.content.decode(encoding))
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
|
def text(self, encoding='utf-8'):
|
||||||
|
return self.content.decode(encoding)
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
|
def release(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def vcr_request(cassette, real_request):
|
||||||
|
@functools.wraps(real_request)
|
||||||
|
@asyncio.coroutine
|
||||||
|
def new_request(self, method, url, **kwargs):
|
||||||
|
headers = kwargs.get('headers')
|
||||||
|
headers = self._prepare_headers(headers)
|
||||||
|
data = kwargs.get('data')
|
||||||
|
params = kwargs.get('params')
|
||||||
|
if params:
|
||||||
|
for k, v in params.items():
|
||||||
|
params[k] = str(v)
|
||||||
|
|
||||||
|
request_url = URL(url).with_query(params)
|
||||||
|
vcr_request = Request(method, str(request_url), data, headers)
|
||||||
|
|
||||||
|
if cassette.can_play_response_for(vcr_request):
|
||||||
|
vcr_response = cassette.play_response(vcr_request)
|
||||||
|
|
||||||
|
response = MockClientResponse(method, URL(vcr_response.get('url')))
|
||||||
|
response.status = vcr_response['status']['code']
|
||||||
|
response.content = vcr_response['body']['string']
|
||||||
|
response.reason = vcr_response['status']['message']
|
||||||
|
response.headers = vcr_response['headers']
|
||||||
|
|
||||||
|
response.close()
|
||||||
|
return response
|
||||||
|
|
||||||
|
if cassette.write_protected and cassette.filter_request(vcr_request):
|
||||||
|
response = MockClientResponse(method, URL(url))
|
||||||
|
response.status = 599
|
||||||
|
msg = ("No match for the request {!r} was found. Can't overwrite "
|
||||||
|
"existing cassette {!r} in your current record mode {!r}.")
|
||||||
|
msg = msg.format(vcr_request, cassette._path, cassette.record_mode)
|
||||||
|
response.content = msg.encode()
|
||||||
|
response.close()
|
||||||
|
return response
|
||||||
|
|
||||||
|
response = yield from real_request(self, method, url, **kwargs) # NOQA: E999
|
||||||
|
|
||||||
|
vcr_response = {
|
||||||
|
'status': {
|
||||||
|
'code': response.status,
|
||||||
|
'message': response.reason,
|
||||||
|
},
|
||||||
|
'headers': dict(response.headers),
|
||||||
|
'body': {'string': (yield from response.text())}, # NOQA: E999
|
||||||
|
'url': response.url,
|
||||||
|
}
|
||||||
|
cassette.append(vcr_request, vcr_response)
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
|
return new_request
|
||||||
Reference in New Issue
Block a user