mirror of
https://github.com/kevin1024/vcrpy.git
synced 2025-12-08 16:53:23 +00:00
Compare commits
162 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0cf11d4525 | ||
|
|
75a334686f | ||
|
|
9a9cdb3a95 | ||
|
|
b38915a89a | ||
|
|
e93060c81b | ||
|
|
10736db427 | ||
|
|
cb4228cf90 | ||
|
|
f7c051cde6 | ||
|
|
075dde6707 | ||
|
|
af2742b6b9 | ||
|
|
e9d00a5e2a | ||
|
|
957db22d5c | ||
|
|
5ddcaa4870 | ||
|
|
76076e5ccb | ||
|
|
7417978e36 | ||
|
|
e559be758a | ||
|
|
bb8d39dd20 | ||
|
|
ff7dd06f47 | ||
|
|
4b6b5effc7 | ||
|
|
06dc2190d6 | ||
|
|
eb4774a7d2 | ||
|
|
bed9e520a3 | ||
|
|
365a98bf66 | ||
|
|
fc95e34bd4 | ||
|
|
236dc1f4f2 | ||
|
|
43f4eb8156 | ||
|
|
aff71c5107 | ||
|
|
506700651d | ||
|
|
d1b11da610 | ||
|
|
306238d561 | ||
|
|
dbddaa0e44 | ||
|
|
0d4c9eccf5 | ||
|
|
1674741d9f | ||
|
|
75cb067e29 | ||
|
|
ab6e6b5b5d | ||
|
|
9e8bd382d3 | ||
|
|
ba79174a1f | ||
|
|
c341e48961 | ||
|
|
5be75692c4 | ||
|
|
b10b92bdbb | ||
|
|
3009cbbbe9 | ||
|
|
f811b41ad9 | ||
|
|
140bc2ee74 | ||
|
|
867fd9ab4b | ||
|
|
545c903ee2 | ||
|
|
cd864b5eca | ||
|
|
689d68a0a2 | ||
|
|
709017ea46 | ||
|
|
8621427f46 | ||
|
|
7e695ff7bc | ||
|
|
bd08e5119f | ||
|
|
6ab508d67d | ||
|
|
f1561ae0f8 | ||
|
|
f1f8ce2af4 | ||
|
|
26be756f47 | ||
|
|
f890709a20 | ||
|
|
d0ae5fa40b | ||
|
|
1562bc7659 | ||
|
|
16b69aa2e5 | ||
|
|
d9caff107d | ||
|
|
f317490eec | ||
|
|
cf13805973 | ||
|
|
389cb4d6e3 | ||
|
|
7a82d70391 | ||
|
|
f3b9966a2a | ||
|
|
5ba1c7fbb6 | ||
|
|
ad153bd733 | ||
|
|
42b3b16fe1 | ||
|
|
531dc02ca5 | ||
|
|
2156adb841 | ||
|
|
6caf7e962e | ||
|
|
97fbd7e0bd | ||
|
|
ead48b1907 | ||
|
|
1af4b2587e | ||
|
|
82fa50c092 | ||
|
|
58d8980cfa | ||
|
|
c111ebab0a | ||
|
|
943a15a967 | ||
|
|
d0aa6bcc8d | ||
|
|
04fd730a08 | ||
|
|
6156271c48 | ||
|
|
87666ba2e4 | ||
|
|
7915d07aff | ||
|
|
095e272191 | ||
|
|
42762ec806 | ||
|
|
bfb38af8e1 | ||
|
|
894695d13b | ||
|
|
a56a0726d4 | ||
|
|
c366852925 | ||
|
|
0cab15658f | ||
|
|
c3ecf8c5b2 | ||
|
|
81d453f7d3 | ||
|
|
262ad903cb | ||
|
|
ec60af0214 | ||
|
|
8cf8d3f69c | ||
|
|
034aeb4f17 | ||
|
|
d59efbc6e0 | ||
|
|
b753a491c9 | ||
|
|
9092b34dd1 | ||
|
|
0a3aaddca2 | ||
|
|
c55d976277 | ||
|
|
47ccddafee | ||
|
|
dcaf813657 | ||
|
|
ef727aaaaf | ||
|
|
ee17233aa0 | ||
|
|
f88294a9e6 | ||
|
|
572da2084d | ||
|
|
88bf8f0aac | ||
|
|
9b59e02374 | ||
|
|
ba290a32d2 | ||
|
|
420c2ceb6f | ||
|
|
ec786f2fd9 | ||
|
|
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 |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,5 +1,7 @@
|
||||
*.pyc
|
||||
.tox
|
||||
.cache
|
||||
.pytest_cache/
|
||||
build/
|
||||
dist/
|
||||
*.egg/
|
||||
|
||||
73
.travis.yml
73
.travis.yml
@@ -7,55 +7,62 @@ env:
|
||||
- secure: LBSEg/gMj4u4Hrpo3zs6Y/1mTpd2RtcN49mZIFgTdbJ9IhpiNPqcEt647Lz94F9Eses2x2WbNuKqZKZZReY7QLbEzU1m0nN5jlaKrjcG5NR5clNABfFFyhgc0jBikyS4abAG8jc2efeaTrFuQwdoF4sE8YiVrkiVj2X5Xoi6sBk=
|
||||
matrix:
|
||||
- TOX_SUFFIX="flakes"
|
||||
- TOX_SUFFIX="requests22"
|
||||
- TOX_SUFFIX="requests23"
|
||||
- TOX_SUFFIX="requests24"
|
||||
- TOX_SUFFIX="requests25"
|
||||
- TOX_SUFFIX="requests26"
|
||||
- TOX_SUFFIX="requests27"
|
||||
- TOX_SUFFIX="requests1"
|
||||
- TOX_SUFFIX="httplib2"
|
||||
- TOX_SUFFIX="boto"
|
||||
- TOX_SUFFIX="boto3"
|
||||
- TOX_SUFFIX="urllib317"
|
||||
- TOX_SUFFIX="urllib319"
|
||||
- TOX_SUFFIX="urllib3110"
|
||||
- TOX_SUFFIX="tornado3"
|
||||
- TOX_SUFFIX="urllib3121"
|
||||
- TOX_SUFFIX="tornado4"
|
||||
- TOX_SUFFIX="aiohttp"
|
||||
matrix:
|
||||
allow_failures:
|
||||
- env: TOX_SUFFIX="boto"
|
||||
- env: TOX_SUFFIX="boto3"
|
||||
exclude:
|
||||
include:
|
||||
- env: TOX_SUFFIX="flakes"
|
||||
python: 2.6
|
||||
- env: TOX_SUFFIX="boto"
|
||||
python: 3.3
|
||||
- env: TOX_SUFFIX="boto"
|
||||
python: 3.4
|
||||
- env: TOX_SUFFIX="requests1"
|
||||
python: 3.4
|
||||
- env: TOX_SUFFIX="requests1"
|
||||
python: 3.5
|
||||
python: 3.7
|
||||
dist: xenial
|
||||
sudo: true
|
||||
- env: TOX_SUFFIX="requests27"
|
||||
python: 3.7
|
||||
dist: xenial
|
||||
sudo: true
|
||||
- env: TOX_SUFFIX="httplib2"
|
||||
python: 3.7
|
||||
dist: xenial
|
||||
sudo: true
|
||||
- env: TOX_SUFFIX="urllib3121"
|
||||
python: 3.7
|
||||
dist: xenial
|
||||
sudo: true
|
||||
- env: TOX_SUFFIX="tornado4"
|
||||
python: 3.7
|
||||
dist: xenial
|
||||
sudo: true
|
||||
- env: TOX_SUFFIX="aiohttp"
|
||||
python: 2.6
|
||||
python: 3.7
|
||||
dist: xenial
|
||||
sudo: true
|
||||
allow_failures:
|
||||
- env: TOX_SUFFIX="boto3"
|
||||
- env: TOX_SUFFIX="aiohttp"
|
||||
python: "pypy3.5-5.9.0"
|
||||
exclude:
|
||||
# Only run flakes on a single Python 2.x and a single 3.x
|
||||
- env: TOX_SUFFIX="flakes"
|
||||
python: 3.4
|
||||
- env: TOX_SUFFIX="flakes"
|
||||
python: 3.5
|
||||
- env: TOX_SUFFIX="flakes"
|
||||
python: pypy
|
||||
- env: TOX_SUFFIX="flakes"
|
||||
python: "pypy3.5-5.9.0"
|
||||
- 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:
|
||||
- 2.6
|
||||
- 2.7
|
||||
- 3.3
|
||||
- 3.4
|
||||
- 3.5
|
||||
- 3.6
|
||||
- pypy
|
||||
- pypy3
|
||||
- "pypy3.5-5.9.0"
|
||||
install:
|
||||
- pip install tox-travis
|
||||
- if [[ $TOX_SUFFIX != 'flakes' ]]; then python setup.py install ; fi
|
||||
|
||||
24
README.rst
24
README.rst
@@ -1,4 +1,4 @@
|
||||
|PyPI| |Build Status| |Waffle Ready| |Gitter|
|
||||
|PyPI| |Python versions| |Build Status| |Waffle Ready| |Gitter|
|
||||
|
||||
VCR.py
|
||||
======
|
||||
@@ -41,20 +41,6 @@ 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
|
||||
API.
|
||||
|
||||
Support
|
||||
-------
|
||||
|
||||
VCR.py works great with the following HTTP clients:
|
||||
|
||||
- requests
|
||||
- aiohttp
|
||||
- urllib3
|
||||
- tornado
|
||||
- urllib2
|
||||
- boto
|
||||
- boto3
|
||||
|
||||
|
||||
License
|
||||
=======
|
||||
|
||||
@@ -62,10 +48,12 @@ This library uses the MIT license. See `LICENSE.txt <LICENSE.txt>`__ for
|
||||
more details
|
||||
|
||||
.. |PyPI| image:: https://img.shields.io/pypi/v/vcrpy.svg
|
||||
:target: https://pypi.python.org/pypi/vcrpy-unittest
|
||||
.. |Build Status| image:: https://secure.travis-ci.org/kevin1024/vcrpy.png?branch=master
|
||||
:target: https://pypi.python.org/pypi/vcrpy
|
||||
.. |Python versions| image:: https://img.shields.io/pypi/pyversions/vcrpy.svg
|
||||
:target: https://pypi.python.org/pypi/vcrpy
|
||||
.. |Build Status| image:: https://secure.travis-ci.org/kevin1024/vcrpy.svg?branch=master
|
||||
:target: http://travis-ci.org/kevin1024/vcrpy
|
||||
.. |Waffle Ready| image:: https://badge.waffle.io/kevin1024/vcrpy.png?label=ready&title=waffle
|
||||
.. |Waffle Ready| image:: https://badge.waffle.io/kevin1024/vcrpy.svg?label=ready&title=waffle
|
||||
:target: https://waffle.io/kevin1024/vcrpy
|
||||
.. |Gitter| image:: https://badges.gitter.im/Join%20Chat.svg
|
||||
:alt: Join the chat at https://gitter.im/kevin1024/vcrpy
|
||||
|
||||
@@ -122,6 +122,27 @@ Finally, register your method with VCR to use your new request matcher.
|
||||
with my_vcr.use_cassette('test.yml'):
|
||||
# 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
|
||||
--------------------------------------
|
||||
|
||||
@@ -201,7 +222,7 @@ Custom Request filtering
|
||||
|
||||
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
|
||||
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
|
||||
endpoint.
|
||||
|
||||
@@ -212,7 +233,7 @@ endpoint.
|
||||
return request
|
||||
|
||||
my_vcr = vcr.VCR(
|
||||
before_record = before_record_cb,
|
||||
before_record_request = before_record_cb,
|
||||
)
|
||||
with my_vcr.use_cassette('test.yml'):
|
||||
# your http code here
|
||||
@@ -229,7 +250,7 @@ path.
|
||||
return request
|
||||
|
||||
my_vcr = vcr.VCR(
|
||||
before_record=scrub_login_request,
|
||||
before_record_request=scrub_login_request,
|
||||
)
|
||||
with my_vcr.use_cassette('test.yml'):
|
||||
# your http code here
|
||||
|
||||
@@ -1,5 +1,27 @@
|
||||
Changelog
|
||||
---------
|
||||
- 2.0.0 - Support python 3.7 (fix httplib2 and urllib2, thanks @felixonmars)
|
||||
[#356] Fixes `before_record_response` so the original response isn't changed (thanks @kgraves)
|
||||
Fix requests stub when using proxy (thanks @samuelfekete @daneoshiga)
|
||||
(only for aiohttp stub) Drop support to python 3.4 asyncio.coroutine (aiohttp doesn't support python it anymore)
|
||||
Fix aiohttp stub to work with aiohttp client (thanks @stj)
|
||||
Fix aiohttp stub to accept content type passed
|
||||
Improve docs (thanks @adamchainz)
|
||||
- 1.13.0 - Fix support to latest aiohttp version (3.3.2). Fix content-type bug in aiohttp stub. Save URL with query params properly when using aiohttp.
|
||||
- 1.12.0 - Fix support to latest aiohttp version (3.2.1), Adapted setup to PEP508, Support binary responses on aiohttp, Dropped support for EOL python versions (2.6 and 3.3)
|
||||
- 1.11.1 Fix compatibility with newest requests and urllib3 releases
|
||||
- 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
|
||||
for response headers when `decode_compressed_response` is enabled (thanks
|
||||
|
||||
@@ -9,18 +9,20 @@ with pip::
|
||||
Compatibility
|
||||
-------------
|
||||
|
||||
VCR.py supports Python 2.6 and 2.7, 3.3, 3.4, and
|
||||
VCR.py supports Python 2.7 and 3.4+, and
|
||||
`pypy <http://pypy.org>`__.
|
||||
|
||||
The following http libraries are supported:
|
||||
The following HTTP libraries are supported:
|
||||
|
||||
- urllib2
|
||||
- urllib3
|
||||
- http.client (python3)
|
||||
- requests (both 1.x and 2.x versions)
|
||||
- httplib2
|
||||
- boto
|
||||
- Tornado's AsyncHTTPClient
|
||||
- ``aiohttp``
|
||||
- ``boto``
|
||||
- ``boto3``
|
||||
- ``http.client``
|
||||
- ``httplib2``
|
||||
- ``requests`` (both 1.x and 2.x versions)
|
||||
- ``tornado.httpclient``
|
||||
- ``urllib2``
|
||||
- ``urllib3``
|
||||
|
||||
Speed
|
||||
-----
|
||||
@@ -40,7 +42,7 @@ rebuilding pyyaml.
|
||||
|
||||
brew install libyaml # Mac with Homebrew
|
||||
apt-get install libyaml-dev # Ubuntu
|
||||
dnf install libyaml-dev # Fedora
|
||||
dnf install libyaml-devel # Fedora
|
||||
|
||||
3. Rebuild pyyaml with libyaml::
|
||||
|
||||
|
||||
@@ -57,7 +57,7 @@ once
|
||||
file.
|
||||
|
||||
It is similar to the new\_episodes record mode, but will prevent new,
|
||||
unexpected requests from being made (i.e. because the request URI
|
||||
unexpected requests from being made (e.g. because the request URI
|
||||
changed).
|
||||
|
||||
once is the default record mode, used when you do not set one.
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
#!/bin/bash
|
||||
|
||||
REQUESTS_CA_BUNDLE=`python -m pytest_httpbin.certs` py.test $1
|
||||
REQUESTS_CA_BUNDLE=`python -m pytest_httpbin.certs` py.test $*
|
||||
|
||||
50
setup.py
50
setup.py
@@ -1,11 +1,9 @@
|
||||
#!/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()
|
||||
|
||||
@@ -24,34 +22,22 @@ class PyTest(TestCommand):
|
||||
sys.exit(errno)
|
||||
|
||||
|
||||
install_requires = ['PyYAML', 'wrapt', 'six>=1.5']
|
||||
|
||||
|
||||
extras_require = {
|
||||
':python_version in "2.4, 2.5, 2.6"':
|
||||
['contextlib2', 'backport_collections', 'mock'],
|
||||
':python_version in "2.7, 3.1, 3.2"': ['contextlib2', 'mock'],
|
||||
}
|
||||
|
||||
|
||||
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(':'):
|
||||
install_requires.extend(value)
|
||||
install_requires = [
|
||||
'PyYAML',
|
||||
'wrapt',
|
||||
'six>=1.5',
|
||||
'contextlib2; python_version=="2.7"',
|
||||
'mock; python_version=="2.7"',
|
||||
'yarl; python_version>="3.4"',
|
||||
]
|
||||
|
||||
excluded_packages = ["tests*"]
|
||||
if sys.version_info[0] == 2:
|
||||
excluded_packages.append("vcr.stubs.aiohttp_stubs")
|
||||
|
||||
setup(
|
||||
name='vcrpy',
|
||||
version='1.10.0',
|
||||
version='2.0.0',
|
||||
description=(
|
||||
"Automatically mock your HTTP interactions to simplify and "
|
||||
"speed up testing"
|
||||
@@ -60,9 +46,9 @@ setup(
|
||||
author='Kevin McCarthy',
|
||||
author_email='me@kevinmccarthy.org',
|
||||
url='https://github.com/kevin1024/vcrpy',
|
||||
packages=find_packages(exclude=("tests*",)),
|
||||
packages=find_packages(exclude=excluded_packages),
|
||||
python_requires='>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*',
|
||||
install_requires=install_requires,
|
||||
extras_require=extras_require,
|
||||
license='MIT',
|
||||
tests_require=['pytest', 'mock', 'pytest-httpbin'],
|
||||
classifiers=[
|
||||
@@ -70,7 +56,15 @@ setup(
|
||||
'Environment :: Console',
|
||||
'Intended Audience :: Developers',
|
||||
'Programming Language :: Python',
|
||||
'Programming Language :: Python :: 2',
|
||||
'Programming Language :: Python :: 2.7',
|
||||
'Programming Language :: Python :: 3',
|
||||
'Programming Language :: Python :: 3.4',
|
||||
'Programming Language :: Python :: 3.5',
|
||||
'Programming Language :: Python :: 3.6',
|
||||
'Programming Language :: Python :: 3.7',
|
||||
'Programming Language :: Python :: Implementation :: CPython',
|
||||
'Programming Language :: Python :: Implementation :: PyPy',
|
||||
'Topic :: Software Development :: Testing',
|
||||
'Topic :: Internet :: WWW/HTTP',
|
||||
'License :: OSI Approved :: MIT License',
|
||||
|
||||
@@ -1,7 +1,33 @@
|
||||
# flake8: noqa
|
||||
import asyncio
|
||||
|
||||
import aiohttp
|
||||
from aiohttp.test_utils import TestClient
|
||||
|
||||
@asyncio.coroutine
|
||||
def aiohttp_request(session, method, url, as_text, **kwargs):
|
||||
response = yield from session.request(method, url, **kwargs) # NOQA: E999
|
||||
return response, (yield from response.text()) if as_text else (yield from response.json()) # NOQA: E999
|
||||
|
||||
async def aiohttp_request(loop, method, url, output='text', encoding='utf-8', content_type=None, **kwargs):
|
||||
session = aiohttp.ClientSession(loop=loop)
|
||||
response_ctx = session.request(method, url, **kwargs)
|
||||
|
||||
response = await response_ctx.__aenter__()
|
||||
if output == 'text':
|
||||
content = await response.text()
|
||||
elif output == 'json':
|
||||
content_type = content_type or 'application/json'
|
||||
content = await response.json(encoding=encoding, content_type=content_type)
|
||||
elif output == 'raw':
|
||||
content = await response.read()
|
||||
|
||||
response_ctx._resp.close()
|
||||
await session.close()
|
||||
|
||||
return response, content
|
||||
|
||||
|
||||
def aiohttp_app():
|
||||
async def hello(request):
|
||||
return aiohttp.web.Response(text='hello')
|
||||
|
||||
app = aiohttp.web.Application()
|
||||
app.router.add_get('/', hello)
|
||||
return app
|
||||
|
||||
@@ -1,28 +1,33 @@
|
||||
import contextlib
|
||||
|
||||
import pytest
|
||||
asyncio = pytest.importorskip("asyncio")
|
||||
aiohttp = pytest.importorskip("aiohttp")
|
||||
|
||||
import asyncio # NOQA
|
||||
import sys # NOQA
|
||||
|
||||
import aiohttp # NOQA
|
||||
import pytest # NOQA
|
||||
import vcr # NOQA
|
||||
|
||||
from .aiohttp_utils import aiohttp_request # NOQA
|
||||
import vcr # noqa: E402
|
||||
from .aiohttp_utils import aiohttp_app, aiohttp_request # noqa: E402
|
||||
|
||||
|
||||
def get(url, as_text=True, **kwargs):
|
||||
loop = asyncio.get_event_loop()
|
||||
with aiohttp.ClientSession() as session:
|
||||
task = loop.create_task(aiohttp_request(session, 'GET', url, as_text, **kwargs))
|
||||
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 post(url, as_text=True, **kwargs):
|
||||
loop = asyncio.get_event_loop()
|
||||
with aiohttp.ClientSession() as session:
|
||||
task = loop.create_task(aiohttp_request(session, 'POST', url, as_text, **kwargs))
|
||||
return loop.run_until_complete(task)
|
||||
def request(method, url, output='text', **kwargs):
|
||||
def run(loop):
|
||||
return aiohttp_request(loop, method, url, output=output, **kwargs)
|
||||
|
||||
return run_in_loop(run)
|
||||
|
||||
|
||||
def get(url, output='text', **kwargs):
|
||||
return request('GET', url, output=output, **kwargs)
|
||||
|
||||
|
||||
def post(url, output='text', **kwargs):
|
||||
return request('POST', url, output='text', **kwargs)
|
||||
|
||||
|
||||
@pytest.fixture(params=["https", "http"])
|
||||
@@ -66,15 +71,28 @@ def test_text(tmpdir, scheme):
|
||||
|
||||
def test_json(tmpdir, scheme):
|
||||
url = scheme + '://httpbin.org/get'
|
||||
headers = {'Content-Type': 'application/json'}
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join('json.yaml'))):
|
||||
_, response_json = get(url, as_text=False)
|
||||
_, response_json = get(url, output='json', headers=headers)
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join('json.yaml'))) as cassette:
|
||||
_, cassette_response_json = get(url, as_text=False)
|
||||
_, cassette_response_json = get(url, output='json', headers=headers)
|
||||
assert cassette_response_json == response_json
|
||||
assert cassette.play_count == 1
|
||||
|
||||
|
||||
def test_binary(tmpdir, scheme):
|
||||
url = scheme + '://httpbin.org/image/png'
|
||||
with vcr.use_cassette(str(tmpdir.join('binary.yaml'))):
|
||||
_, response_binary = get(url, output='raw')
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join('binary.yaml'))) as cassette:
|
||||
_, cassette_response_binary = get(url, output='raw')
|
||||
assert cassette_response_binary == response_binary
|
||||
assert cassette.play_count == 1
|
||||
|
||||
|
||||
def test_post(tmpdir, scheme):
|
||||
data = {'key1': 'value1', 'key2': 'value2'}
|
||||
url = scheme + '://httpbin.org/post'
|
||||
@@ -85,3 +103,77 @@ def test_post(tmpdir, scheme):
|
||||
_, 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'
|
||||
headers = {'Content-Type': 'application/json'}
|
||||
params = {'a': 1, 'b': False, 'c': 'c'}
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join('get.yaml'))) as cassette:
|
||||
_, response_json = get(url, output='json', params=params, headers=headers)
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join('get.yaml'))) as cassette:
|
||||
_, cassette_response_json = get(url, output='json', params=params, headers=headers)
|
||||
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'
|
||||
headers = {'Content-Type': 'application/json'}
|
||||
params = {'a': 1, 'b': False, 'c': 'c'}
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join('get.yaml'))) as cassette:
|
||||
_, response_json = get(url, output='json', params=params, headers=headers)
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join('get.yaml'))) as cassette:
|
||||
_, cassette_response_json = get(url, output='json', params=params, headers=headers)
|
||||
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, output='text', params=other_params)
|
||||
assert 'No match for the request' in cassette_response_text
|
||||
assert response.status == 599
|
||||
|
||||
|
||||
def test_params_on_url(tmpdir, scheme):
|
||||
url = scheme + '://httpbin.org/get?a=1&b=foo'
|
||||
headers = {'Content-Type': 'application/json'}
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join('get.yaml'))) as cassette:
|
||||
_, response_json = get(url, output='json', headers=headers)
|
||||
request = cassette.requests[0]
|
||||
assert request.url == url
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join('get.yaml'))) as cassette:
|
||||
_, cassette_response_json = get(url, output='json', headers=headers)
|
||||
request = cassette.requests[0]
|
||||
assert request.url == url
|
||||
assert cassette_response_json == response_json
|
||||
assert cassette.play_count == 1
|
||||
|
||||
|
||||
def test_aiohttp_test_client(aiohttp_client, tmpdir):
|
||||
loop = asyncio.get_event_loop()
|
||||
app = aiohttp_app()
|
||||
url = '/'
|
||||
client = loop.run_until_complete(aiohttp_client(app))
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join('get.yaml'))):
|
||||
response = loop.run_until_complete(client.get(url))
|
||||
|
||||
assert response.status == 200
|
||||
response_text = loop.run_until_complete(response.text())
|
||||
assert response_text == 'hello'
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join('get.yaml'))) as cassette:
|
||||
response = loop.run_until_complete(client.get(url))
|
||||
|
||||
request = cassette.requests[0]
|
||||
assert request.url == str(client.make_url(url))
|
||||
response_text = loop.run_until_complete(response.text())
|
||||
assert response_text == 'hello'
|
||||
assert cassette.play_count == 1
|
||||
|
||||
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
|
||||
@@ -1,12 +1,12 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
'''Integration tests with httplib2'''
|
||||
|
||||
# External imports
|
||||
import sys
|
||||
|
||||
from six.moves.urllib_parse import urlencode
|
||||
import pytest
|
||||
import pytest_httpbin.certs
|
||||
|
||||
# Internal imports
|
||||
import vcr
|
||||
|
||||
from assertions import assert_cassette_has_one_response
|
||||
@@ -19,7 +19,12 @@ def http():
|
||||
Returns an httplib2 HTTP instance
|
||||
with the certificate replaced by the httpbin one.
|
||||
"""
|
||||
return httplib2.Http(ca_certs=pytest_httpbin.certs.where())
|
||||
kwargs = {
|
||||
'ca_certs': pytest_httpbin.certs.where()
|
||||
}
|
||||
if sys.version_info[:2] == (3, 7):
|
||||
kwargs['disable_ssl_certificate_validation'] = True
|
||||
return httplib2.Http(**kwargs)
|
||||
|
||||
|
||||
def test_response_code(tmpdir, httpbin_both):
|
||||
|
||||
@@ -26,9 +26,9 @@ def test_ignore_localhost(tmpdir, httpbin):
|
||||
with overridden_dns({'httpbin.org': '127.0.0.1'}):
|
||||
cass_file = str(tmpdir.join('filter_qs.yaml'))
|
||||
with vcr.use_cassette(cass_file, ignore_localhost=True) as cass:
|
||||
urlopen('http://localhost:{0}/'.format(httpbin.port))
|
||||
urlopen('http://localhost:{}/'.format(httpbin.port))
|
||||
assert len(cass) == 0
|
||||
urlopen('http://httpbin.org:{0}/'.format(httpbin.port))
|
||||
urlopen('http://httpbin.org:{}/'.format(httpbin.port))
|
||||
assert len(cass) == 1
|
||||
|
||||
|
||||
@@ -39,9 +39,9 @@ def test_ignore_httpbin(tmpdir, httpbin):
|
||||
cass_file,
|
||||
ignore_hosts=['httpbin.org']
|
||||
) as cass:
|
||||
urlopen('http://httpbin.org:{0}/'.format(httpbin.port))
|
||||
urlopen('http://httpbin.org:{}/'.format(httpbin.port))
|
||||
assert len(cass) == 0
|
||||
urlopen('http://localhost:{0}/'.format(httpbin.port))
|
||||
urlopen('http://localhost:{}/'.format(httpbin.port))
|
||||
assert len(cass) == 1
|
||||
|
||||
|
||||
@@ -53,8 +53,8 @@ def test_ignore_localhost_and_httpbin(tmpdir, httpbin):
|
||||
ignore_hosts=['httpbin.org'],
|
||||
ignore_localhost=True
|
||||
) as cass:
|
||||
urlopen('http://httpbin.org:{0}'.format(httpbin.port))
|
||||
urlopen('http://localhost:{0}'.format(httpbin.port))
|
||||
urlopen('http://httpbin.org:{}'.format(httpbin.port))
|
||||
urlopen('http://localhost:{}'.format(httpbin.port))
|
||||
assert len(cass) == 0
|
||||
|
||||
|
||||
@@ -62,12 +62,12 @@ def test_ignore_localhost_twice(tmpdir, httpbin):
|
||||
with overridden_dns({'httpbin.org': '127.0.0.1'}):
|
||||
cass_file = str(tmpdir.join('filter_qs.yaml'))
|
||||
with vcr.use_cassette(cass_file, ignore_localhost=True) as cass:
|
||||
urlopen('http://localhost:{0}'.format(httpbin.port))
|
||||
urlopen('http://localhost:{}'.format(httpbin.port))
|
||||
assert len(cass) == 0
|
||||
urlopen('http://httpbin.org:{0}'.format(httpbin.port))
|
||||
urlopen('http://httpbin.org:{}'.format(httpbin.port))
|
||||
assert len(cass) == 1
|
||||
with vcr.use_cassette(cass_file, ignore_localhost=True) as cass:
|
||||
assert len(cass) == 1
|
||||
urlopen('http://localhost:{0}'.format(httpbin.port))
|
||||
urlopen('http://httpbin.org:{0}'.format(httpbin.port))
|
||||
urlopen('http://localhost:{}'.format(httpbin.port))
|
||||
urlopen('http://httpbin.org:{}'.format(httpbin.port))
|
||||
assert len(cass) == 1
|
||||
|
||||
60
tests/integration/test_proxy.py
Normal file
60
tests/integration/test_proxy.py
Normal file
@@ -0,0 +1,60 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
'''Test using a proxy.'''
|
||||
|
||||
# External imports
|
||||
import multiprocessing
|
||||
import pytest
|
||||
|
||||
from six.moves import socketserver, SimpleHTTPServer
|
||||
from six.moves.urllib.request import urlopen
|
||||
|
||||
# Internal imports
|
||||
import vcr
|
||||
|
||||
# Conditional imports
|
||||
requests = pytest.importorskip("requests")
|
||||
|
||||
|
||||
class Proxy(SimpleHTTPServer.SimpleHTTPRequestHandler):
|
||||
'''
|
||||
Simple proxy server.
|
||||
|
||||
(Inspired by: http://effbot.org/librarybook/simplehttpserver.htm).
|
||||
'''
|
||||
def do_GET(self):
|
||||
upstream_response = urlopen(self.path)
|
||||
try:
|
||||
status = upstream_response.status
|
||||
headers = upstream_response.headers.items()
|
||||
except AttributeError:
|
||||
# In Python 2 the response is an addinfourl instance.
|
||||
status = upstream_response.code
|
||||
headers = upstream_response.info().items()
|
||||
self.send_response(status, upstream_response.msg)
|
||||
for header in headers:
|
||||
self.send_header(*header)
|
||||
self.end_headers()
|
||||
self.copyfile(upstream_response, self.wfile)
|
||||
|
||||
|
||||
@pytest.yield_fixture(scope='session')
|
||||
def proxy_server():
|
||||
httpd = socketserver.ThreadingTCPServer(('', 0), Proxy)
|
||||
proxy_process = multiprocessing.Process(
|
||||
target=httpd.serve_forever,
|
||||
)
|
||||
proxy_process.start()
|
||||
yield 'http://{0}:{1}'.format(*httpd.server_address)
|
||||
proxy_process.terminate()
|
||||
|
||||
|
||||
def test_use_proxy(tmpdir, httpbin, proxy_server):
|
||||
'''Ensure that it works with a proxy.'''
|
||||
with vcr.use_cassette(str(tmpdir.join('proxy.yaml'))):
|
||||
response = requests.get(httpbin.url, proxies={'http': proxy_server})
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join('proxy.yaml'))) as cassette:
|
||||
cassette_response = requests.get(httpbin.url, proxies={'http': proxy_server})
|
||||
|
||||
assert cassette_response.headers == response.headers
|
||||
assert cassette.play_count == 1
|
||||
55
tests/integration/test_register_persister.py
Normal file
55
tests/integration/test_register_persister.py
Normal file
@@ -0,0 +1,55 @@
|
||||
# -*- 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
|
||||
|
||||
|
||||
class CustomFilesystemPersister(object):
|
||||
'''Behaves just like default FilesystemPersister but adds .test extension
|
||||
to the cassette file'''
|
||||
@staticmethod
|
||||
def load_cassette(cassette_path, serializer):
|
||||
cassette_path += '.test'
|
||||
return FilesystemPersister.load_cassette(cassette_path, serializer)
|
||||
|
||||
@staticmethod
|
||||
def save_cassette(cassette_path, cassette_dict, serializer):
|
||||
cassette_path += '.test'
|
||||
FilesystemPersister.save_cassette(cassette_path, cassette_dict,
|
||||
serializer)
|
||||
|
||||
|
||||
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(CustomFilesystemPersister)
|
||||
|
||||
# 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.test')))
|
||||
|
||||
|
||||
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(CustomFilesystemPersister)
|
||||
|
||||
test_fixture = str(tmpdir.join('synopsis.json.test'))
|
||||
|
||||
with my_vcr.use_cassette(test_fixture, serializer='json'):
|
||||
response = urlopen(httpbin.url).read()
|
||||
assert b'difficult sometimes' in response
|
||||
@@ -1,11 +1,13 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
'''Test requests' interaction with vcr'''
|
||||
import platform
|
||||
import pytest
|
||||
import sys
|
||||
import vcr
|
||||
from assertions import assert_cassette_empty, assert_is_json
|
||||
|
||||
|
||||
requests = pytest.importorskip("requests")
|
||||
from requests.exceptions import ConnectionError # noqa E402
|
||||
|
||||
|
||||
def test_status_code(httpbin_both, tmpdir):
|
||||
@@ -38,6 +40,18 @@ def test_body(tmpdir, httpbin_both):
|
||||
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):
|
||||
'''Ensure that the effective_url is captured'''
|
||||
url = httpbin_both.url + '/redirect-to?url=/html'
|
||||
@@ -88,11 +102,29 @@ def test_post(tmpdir, httpbin_both):
|
||||
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.'''
|
||||
data1 = 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.xskip('sys.version_info >= (3, 6)', strict=True, raises=ConnectionError)
|
||||
@pytest.mark.xskip((3, 5) < sys.version_info < (3, 6) and
|
||||
platform.python_implementation() == 'CPython',
|
||||
reason='Fails on CPython 3.5')
|
||||
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'))):
|
||||
req1 = requests.post(url, data1).content
|
||||
print(req1)
|
||||
@@ -222,10 +254,8 @@ def test_nested_cassettes_with_session_created_before_nesting(httpbin_both, tmpd
|
||||
def test_post_file(tmpdir, httpbin_both):
|
||||
'''Ensure that we handle posting a file.'''
|
||||
url = httpbin_both + '/post'
|
||||
with vcr.use_cassette(str(tmpdir.join('post_file.yaml'))) as cass:
|
||||
# Don't use 2.7+ only style ',' separated with here because we support python 2.6
|
||||
with open('tox.ini') as f:
|
||||
original_response = requests.post(url, f).content
|
||||
with vcr.use_cassette(str(tmpdir.join('post_file.yaml'))) as cass, open('tox.ini') as f:
|
||||
original_response = requests.post(url, f).content
|
||||
|
||||
# This also tests that we do the right thing with matching the body when they are files.
|
||||
with vcr.use_cassette(str(tmpdir.join('post_file.yaml')),
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import vcr
|
||||
import zlib
|
||||
import json
|
||||
import six.moves.http_client as httplib
|
||||
|
||||
from assertions import assert_is_json
|
||||
@@ -83,3 +84,50 @@ def test_original_decoded_response_is_not_modified(tmpdir, httpbin):
|
||||
|
||||
assert 'content-encoding' not in inside.headers
|
||||
assert_is_json(inside.read())
|
||||
|
||||
|
||||
def _make_before_record_response(fields, replacement='[REDACTED]'):
|
||||
def before_record_response(response):
|
||||
string_body = response['body']['string'].decode('utf8')
|
||||
body = json.loads(string_body)
|
||||
|
||||
for field in fields:
|
||||
if field in body:
|
||||
body[field] = replacement
|
||||
|
||||
response['body']['string'] = json.dumps(body).encode()
|
||||
return response
|
||||
return before_record_response
|
||||
|
||||
|
||||
def test_original_response_is_not_modified_by_before_filter(tmpdir, httpbin):
|
||||
testfile = str(tmpdir.join('sensitive_data_scrubbed_response.yml'))
|
||||
host, port = httpbin.host, httpbin.port
|
||||
field_to_scrub = 'url'
|
||||
replacement = '[YOU_CANT_HAVE_THE_MANGO]'
|
||||
|
||||
conn = httplib.HTTPConnection(host, port)
|
||||
conn.request('GET', '/get')
|
||||
outside = conn.getresponse()
|
||||
|
||||
callback = _make_before_record_response([field_to_scrub], replacement)
|
||||
with vcr.use_cassette(testfile, before_record_response=callback):
|
||||
conn = httplib.HTTPConnection(host, port)
|
||||
conn.request('GET', '/get')
|
||||
inside = conn.getresponse()
|
||||
|
||||
# The scrubbed field should be the same, because no cassette existed.
|
||||
# Furthermore, the responses should be identical.
|
||||
inside_body = json.loads(inside.read().decode('utf-8'))
|
||||
outside_body = json.loads(outside.read().decode('utf-8'))
|
||||
assert not inside_body[field_to_scrub] == replacement
|
||||
assert inside_body[field_to_scrub] == outside_body[field_to_scrub]
|
||||
|
||||
# Ensure that when a cassette exists, the scrubbed response is returned.
|
||||
with vcr.use_cassette(testfile, before_record_response=callback):
|
||||
conn = httplib.HTTPConnection(host, port)
|
||||
conn.request('GET', '/get')
|
||||
inside = conn.getresponse()
|
||||
|
||||
inside_body = json.loads(inside.read().decode('utf-8'))
|
||||
assert inside_body[field_to_scrub] == replacement
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
'''Integration tests with urllib2'''
|
||||
|
||||
import ssl
|
||||
from six.moves.urllib.request import urlopen
|
||||
from six.moves.urllib_parse import urlencode
|
||||
import pytest_httpbin.certs
|
||||
@@ -12,7 +13,9 @@ from assertions import assert_cassette_has_one_response
|
||||
|
||||
|
||||
def urlopen_with_cafile(*args, **kwargs):
|
||||
kwargs['cafile'] = pytest_httpbin.certs.where()
|
||||
context = ssl.create_default_context(cafile=pytest_httpbin.certs.where())
|
||||
context.check_hostname = False
|
||||
kwargs['context'] = context
|
||||
try:
|
||||
return urlopen(*args, **kwargs)
|
||||
except TypeError:
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
import pytest
|
||||
import pytest_httpbin
|
||||
import vcr
|
||||
from vcr.patch import force_reset
|
||||
from assertions import assert_cassette_empty, assert_is_json
|
||||
urllib3 = pytest.importorskip("urllib3")
|
||||
|
||||
@@ -55,7 +56,7 @@ def test_body(tmpdir, httpbin_both, verify_pool_mgr):
|
||||
def test_auth(tmpdir, httpbin_both, verify_pool_mgr):
|
||||
'''Ensure that we can handle basic auth'''
|
||||
auth = ('user', 'passwd')
|
||||
headers = urllib3.util.make_headers(basic_auth='{0}:{1}'.format(*auth))
|
||||
headers = urllib3.util.make_headers(basic_auth='{}:{}'.format(*auth))
|
||||
url = httpbin_both.url + '/basic-auth/user/passwd'
|
||||
with vcr.use_cassette(str(tmpdir.join('auth.yaml'))):
|
||||
one = verify_pool_mgr.request('GET', url, headers=headers)
|
||||
@@ -69,7 +70,7 @@ def test_auth(tmpdir, httpbin_both, verify_pool_mgr):
|
||||
def test_auth_failed(tmpdir, httpbin_both, verify_pool_mgr):
|
||||
'''Ensure that we can save failed auth statuses'''
|
||||
auth = ('user', 'wrongwrongwrong')
|
||||
headers = urllib3.util.make_headers(basic_auth='{0}:{1}'.format(*auth))
|
||||
headers = urllib3.util.make_headers(basic_auth='{}:{}'.format(*auth))
|
||||
url = httpbin_both.url + '/basic-auth/user/passwd'
|
||||
with vcr.use_cassette(str(tmpdir.join('auth-failed.yaml'))) as cass:
|
||||
# Ensure that this is empty to begin with
|
||||
@@ -138,3 +139,21 @@ def test_gzip(tmpdir, httpbin_both, verify_pool_mgr):
|
||||
def test_https_with_cert_validation_disabled(tmpdir, httpbin_secure, pool_mgr):
|
||||
with vcr.use_cassette(str(tmpdir.join('cert_validation_disabled.yaml'))):
|
||||
pool_mgr.request('GET', httpbin_secure.url)
|
||||
|
||||
|
||||
def test_urllib3_force_reset():
|
||||
cpool = urllib3.connectionpool
|
||||
http_original = cpool.HTTPConnection
|
||||
https_original = cpool.HTTPSConnection
|
||||
verified_https_original = cpool.VerifiedHTTPSConnection
|
||||
with vcr.use_cassette(path='test'):
|
||||
first_cassette_HTTPConnection = cpool.HTTPConnection
|
||||
first_cassette_HTTPSConnection = cpool.HTTPSConnection
|
||||
first_cassette_VerifiedHTTPSConnection = cpool.VerifiedHTTPSConnection
|
||||
with force_reset():
|
||||
assert cpool.HTTPConnection is http_original
|
||||
assert cpool.HTTPSConnection is https_original
|
||||
assert cpool.VerifiedHTTPSConnection is verified_https_original
|
||||
assert cpool.HTTPConnection is first_cassette_HTTPConnection
|
||||
assert cpool.HTTPSConnection is first_cassette_HTTPSConnection
|
||||
assert cpool.VerifiedHTTPSConnection is first_cassette_VerifiedHTTPSConnection
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import multiprocessing
|
||||
import pytest
|
||||
from six.moves import xmlrpc_client
|
||||
from six.moves import xmlrpc_client, xmlrpc_server
|
||||
|
||||
requests = pytest.importorskip("requests")
|
||||
|
||||
@@ -80,13 +81,27 @@ def test_amazon_doctype(tmpdir):
|
||||
assert 'html' in r.text
|
||||
|
||||
|
||||
def test_xmlrpclib(tmpdir):
|
||||
@pytest.yield_fixture(scope='session')
|
||||
def rpc_server():
|
||||
httpd = xmlrpc_server.SimpleXMLRPCServer(('', 0))
|
||||
httpd.register_function(pow)
|
||||
proxy_process = multiprocessing.Process(
|
||||
target=httpd.serve_forever,
|
||||
)
|
||||
try:
|
||||
proxy_process.start()
|
||||
yield 'http://{}:{}'.format(*httpd.server_address)
|
||||
finally:
|
||||
proxy_process.terminate()
|
||||
|
||||
|
||||
def test_xmlrpclib(tmpdir, rpc_server):
|
||||
with vcr.use_cassette(str(tmpdir.join('xmlrpcvideo.yaml'))):
|
||||
roundup_server = xmlrpc_client.ServerProxy('http://bugs.python.org/xmlrpc', allow_none=True)
|
||||
original_schema = roundup_server.schema()
|
||||
roundup_server = xmlrpc_client.ServerProxy(rpc_server, allow_none=True)
|
||||
original_schema = roundup_server.pow(2, 4)
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join('xmlrpcvideo.yaml'))):
|
||||
roundup_server = xmlrpc_client.ServerProxy('http://bugs.python.org/xmlrpc', allow_none=True)
|
||||
second_schema = roundup_server.schema()
|
||||
roundup_server = xmlrpc_client.ServerProxy(rpc_server, allow_none=True)
|
||||
second_schema = roundup_server.pow(2, 4)
|
||||
|
||||
assert original_schema == second_schema
|
||||
|
||||
@@ -83,7 +83,8 @@ def make_get_request():
|
||||
|
||||
|
||||
@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.stubs.VCRHTTPResponse')
|
||||
def test_function_decorated_with_use_cassette_can_be_invoked_multiple_times(*args):
|
||||
|
||||
@@ -22,7 +22,7 @@ def assert_matcher(matcher_name):
|
||||
matcher = getattr(matchers, matcher_name)
|
||||
for k1, k2 in itertools.permutations(REQUESTS, 2):
|
||||
matched = matcher(REQUESTS[k1], REQUESTS[k2])
|
||||
if matcher_name in set((k1, k2)):
|
||||
if matcher_name in {k1, k2}:
|
||||
assert not matched
|
||||
else:
|
||||
assert matched
|
||||
@@ -31,7 +31,7 @@ def assert_matcher(matcher_name):
|
||||
def test_uri_matcher():
|
||||
for k1, k2 in itertools.permutations(REQUESTS, 2):
|
||||
matched = matchers.uri(REQUESTS[k1], REQUESTS[k2])
|
||||
if set((k1, k2)) != set(('base', 'method')):
|
||||
if {k1, k2} != {'base', 'method'}:
|
||||
assert not matched
|
||||
else:
|
||||
assert matched
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import pytest
|
||||
|
||||
import vcr.persist
|
||||
from vcr.persisters.filesystem import FilesystemPersister
|
||||
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):
|
||||
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()
|
||||
|
||||
|
||||
@@ -20,5 +20,5 @@ def test_load_cassette_with_old_cassettes(cassette_path, serializer):
|
||||
])
|
||||
def test_load_cassette_with_invalid_cassettes(cassette_path, serializer):
|
||||
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()
|
||||
|
||||
@@ -4,7 +4,7 @@ import pytest
|
||||
from vcr.compat import mock
|
||||
from vcr.request import Request
|
||||
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():
|
||||
@@ -131,3 +131,9 @@ def test_serialize_binary_request():
|
||||
)
|
||||
except (UnicodeDecodeError, TypeError) as 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.compat import mock
|
||||
from vcr.cassette import Cassette
|
||||
|
||||
|
||||
class TestVCRConnection(object):
|
||||
@@ -7,3 +9,10 @@ class TestVCRConnection(object):
|
||||
vcr_connection = VCRHTTPSConnection('www.examplehost.com')
|
||||
vcr_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
|
||||
|
||||
# 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
|
||||
mock_filter = mock.Mock()
|
||||
@@ -118,7 +118,7 @@ def test_before_record_response_as_filter():
|
||||
response = object() # just can't be None
|
||||
|
||||
# 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)
|
||||
vcr = VCR(before_record_response=filter_all)
|
||||
@@ -132,7 +132,7 @@ def test_vcr_path_transformer():
|
||||
# Regression test for #199
|
||||
|
||||
# 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
|
||||
vcr = VCR()
|
||||
@@ -319,11 +319,11 @@ def test_additional_matchers():
|
||||
|
||||
@vcr.use_cassette
|
||||
def function_defaults(cassette):
|
||||
assert set(cassette._match_on) == set([vcr.matchers['uri']])
|
||||
assert set(cassette._match_on) == {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']])
|
||||
assert set(cassette._match_on) == {vcr.matchers['uri'], vcr.matchers['body']}
|
||||
|
||||
function_defaults()
|
||||
function_additional()
|
||||
|
||||
27
tox.ini
27
tox.ini
@@ -1,5 +1,5 @@
|
||||
[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 = {py27,py35,py36,py37,pypy}-{flakes,requests27,httplib2,urllib3121,tornado4,boto3,aiohttp}
|
||||
|
||||
[testenv:flakes]
|
||||
skipsdist = True
|
||||
@@ -13,32 +13,21 @@ deps = flake8
|
||||
commands =
|
||||
./runtests.sh {posargs}
|
||||
deps =
|
||||
# httpbin fails with latest Flask, so we pin it
|
||||
Flask==0.10.1
|
||||
Flask<1
|
||||
mock
|
||||
pytest
|
||||
pytest-httpbin
|
||||
PyYAML
|
||||
requests1: requests==1.2.3
|
||||
requests27: requests==2.7.0
|
||||
requests26: requests==2.6.0
|
||||
requests25: requests==2.5.0
|
||||
requests24: requests==2.4.0
|
||||
requests23: requests==2.3.0
|
||||
requests22: requests==2.2.1
|
||||
httplib2: httplib2
|
||||
urllib317: urllib3==1.7.1
|
||||
urllib319: urllib3==1.9.1
|
||||
urllib3110: urllib3==1.10.2
|
||||
{py26,py27,py33,py34,pypy}-tornado3: tornado>=3,<4
|
||||
{py26,py27,py33,py34,pypy}-tornado4: tornado>=4,<5
|
||||
{py26,py27,py33,py34,pypy}-tornado3: pytest-tornado
|
||||
{py26,py27,py33,py34,pypy}-tornado4: pytest-tornado
|
||||
{py26,py27,py33,py34}-tornado3: pycurl
|
||||
{py26,py27,py33,py34}-tornado4: pycurl
|
||||
boto: boto
|
||||
urllib3121: urllib3==1.21.1
|
||||
{py27,py35,py36,pypy}-tornado4: tornado>=4,<5
|
||||
{py27,py35,py36,pypy}-tornado4: pytest-tornado
|
||||
{py27,py35,py36}-tornado4: pycurl
|
||||
boto3: boto3
|
||||
aiohttp: aiohttp
|
||||
aiohttp: pytest-asyncio
|
||||
aiohttp: pytest-aiohttp
|
||||
|
||||
[flake8]
|
||||
max_line_length = 110
|
||||
|
||||
3
vcr/_handle_coroutine.py
Normal file
3
vcr/_handle_coroutine.py
Normal file
@@ -0,0 +1,3 @@
|
||||
async def handle_coroutine(vcr, fn): # noqa: E999
|
||||
with vcr as cassette:
|
||||
return (await fn(cassette)) # noqa: E999
|
||||
@@ -1,17 +1,29 @@
|
||||
import collections
|
||||
import copy
|
||||
import sys
|
||||
import inspect
|
||||
import logging
|
||||
|
||||
import wrapt
|
||||
|
||||
from .compat import contextlib, collections
|
||||
from .compat import contextlib
|
||||
from .errors import UnhandledHTTPRequestError
|
||||
from .matchers import requests_match, uri, method
|
||||
from .patch import CassettePatcherBuilder
|
||||
from .persist import load_cassette, save_cassette
|
||||
from .serializers import yamlserializer
|
||||
from .persisters.filesystem import FilesystemPersister
|
||||
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__)
|
||||
|
||||
@@ -96,18 +108,25 @@ class CassetteContextDecorator(object):
|
||||
)
|
||||
|
||||
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_function(cassette):
|
||||
if cassette.inject:
|
||||
return function(cassette, *args, **kwargs)
|
||||
else:
|
||||
return 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.
|
||||
if iscoroutinefunction(function):
|
||||
return handle_coroutine(vcr=self, fn=handle_function)
|
||||
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:
|
||||
coroutine = self.__handle_function(cassette, function, args, kwargs)
|
||||
coroutine = fn(cassette)
|
||||
# We don't need to catch StopIteration. The caller (Tornado's
|
||||
# gen.coroutine, for example) will handle that.
|
||||
to_yield = next(coroutine)
|
||||
@@ -117,17 +136,14 @@ class CassetteContextDecorator(object):
|
||||
except Exception:
|
||||
to_yield = coroutine.throw(*sys.exc_info())
|
||||
else:
|
||||
to_yield = coroutine.send(to_send)
|
||||
try:
|
||||
to_yield = coroutine.send(to_send)
|
||||
except StopIteration:
|
||||
break
|
||||
|
||||
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):
|
||||
def _handle_function(self, fn):
|
||||
with self as cassette:
|
||||
return self.__handle_function(cassette, function, args, kwargs)
|
||||
return fn(cassette)
|
||||
|
||||
@staticmethod
|
||||
def get_function_name(function):
|
||||
@@ -163,13 +179,13 @@ class Cassette(object):
|
||||
def use(cls, **kwargs):
|
||||
return CassetteContextDecorator.from_args(cls, **kwargs)
|
||||
|
||||
def __init__(self, path, serializer=yamlserializer, record_mode='once',
|
||||
def __init__(self, path, serializer=None, persister=None, record_mode='once',
|
||||
match_on=(uri, method), before_record_request=None,
|
||||
before_record_response=None, custom_patches=(),
|
||||
inject=False):
|
||||
|
||||
self._persister = persister or FilesystemPersister
|
||||
self._path = path
|
||||
self._serializer = serializer
|
||||
self._serializer = serializer or yamlserializer
|
||||
self._match_on = match_on
|
||||
self._before_record_request = before_record_request or (lambda x: x)
|
||||
self._before_record_response = before_record_response or (lambda x: x)
|
||||
@@ -210,6 +226,9 @@ class Cassette(object):
|
||||
request = self._before_record_request(request)
|
||||
if not request:
|
||||
return
|
||||
# Deepcopy is here because mutation of `response` will corrupt the
|
||||
# real response.
|
||||
response = copy.deepcopy(response)
|
||||
response = self._before_record_response(response)
|
||||
if response is None:
|
||||
return
|
||||
@@ -271,28 +290,28 @@ class Cassette(object):
|
||||
|
||||
def _save(self, force=False):
|
||||
if force or self.dirty:
|
||||
save_cassette(
|
||||
self._persister.save_cassette(
|
||||
self._path,
|
||||
self._as_dict(),
|
||||
serializer=self._serializer
|
||||
serializer=self._serializer,
|
||||
)
|
||||
self.dirty = False
|
||||
|
||||
def _load(self):
|
||||
try:
|
||||
requests, responses = load_cassette(
|
||||
requests, responses = self._persister.load_cassette(
|
||||
self._path,
|
||||
serializer=self._serializer
|
||||
serializer=self._serializer,
|
||||
)
|
||||
for request, response in zip(requests, responses):
|
||||
self.append(request, response)
|
||||
self.dirty = False
|
||||
self.rewound = True
|
||||
except IOError:
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
def __str__(self):
|
||||
return "<Cassette containing {0} recorded response(s)>".format(
|
||||
return "<Cassette containing {} recorded response(s)>".format(
|
||||
len(self)
|
||||
)
|
||||
|
||||
|
||||
@@ -11,8 +11,4 @@ else:
|
||||
if not hasattr(contextlib, 'ExitStack'):
|
||||
import contextlib2 as contextlib
|
||||
|
||||
import collections
|
||||
if not hasattr(collections, 'Counter'):
|
||||
import backport_collections as collections
|
||||
|
||||
__all__ = ['mock', 'contextlib', 'collections']
|
||||
__all__ = ['mock', 'contextlib']
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import copy
|
||||
import collections
|
||||
import functools
|
||||
import inspect
|
||||
import os
|
||||
@@ -6,9 +7,9 @@ import types
|
||||
|
||||
import six
|
||||
|
||||
from .compat import collections
|
||||
from .cassette import Cassette
|
||||
from .serializers import yamlserializer, jsonserializer
|
||||
from .persisters.filesystem import FilesystemPersister
|
||||
from .util import compose, auto_decorate
|
||||
from . import matchers
|
||||
from . import filters
|
||||
@@ -57,6 +58,7 @@ class VCR(object):
|
||||
'raw_body': matchers.raw_body,
|
||||
'body': matchers.body,
|
||||
}
|
||||
self.persister = FilesystemPersister
|
||||
self.record_mode = record_mode
|
||||
self.filter_headers = filter_headers
|
||||
self.filter_query_parameters = filter_query_parameters
|
||||
@@ -76,7 +78,7 @@ class VCR(object):
|
||||
serializer = self.serializers[serializer_name]
|
||||
except KeyError:
|
||||
raise KeyError(
|
||||
"Serializer {0} doesn't exist or isn't registered".format(
|
||||
"Serializer {} doesn't exist or isn't registered".format(
|
||||
serializer_name
|
||||
)
|
||||
)
|
||||
@@ -89,7 +91,7 @@ class VCR(object):
|
||||
matchers.append(self.matchers[m])
|
||||
except KeyError:
|
||||
raise KeyError(
|
||||
"Matcher {0} doesn't exist or isn't registered".format(m)
|
||||
"Matcher {} doesn't exist or isn't registered".format(m)
|
||||
)
|
||||
return matchers
|
||||
|
||||
@@ -143,6 +145,7 @@ class VCR(object):
|
||||
|
||||
merged_config = {
|
||||
'serializer': self._get_serializer(serializer_name),
|
||||
'persister': self.persister,
|
||||
'match_on': self._get_matchers(
|
||||
tuple(matcher_names) + tuple(additional_matchers)
|
||||
),
|
||||
@@ -270,6 +273,10 @@ class VCR(object):
|
||||
def register_matcher(self, name, matcher):
|
||||
self.matchers[name] = matcher
|
||||
|
||||
def register_persister(self, persister):
|
||||
# Singleton, no name required
|
||||
self.persister = persister
|
||||
|
||||
def test_case(self, predicate=None):
|
||||
predicate = predicate or self.is_test_method
|
||||
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
|
||||
# string. RFC 7159 says the default encoding is UTF-8 (although UTF-16
|
||||
# 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')
|
||||
@@ -89,12 +90,12 @@ def _log_matches(r1, r2, matches):
|
||||
differences = [m for m in matches if not m[0]]
|
||||
if differences:
|
||||
log.debug(
|
||||
"Requests {0} and {1} differ according to "
|
||||
"the following matchers: {2}".format(r1, r2, differences)
|
||||
"Requests {} and {} differ according to "
|
||||
"the following matchers: {}".format(r1, r2, differences)
|
||||
)
|
||||
|
||||
|
||||
def requests_match(r1, r2, matchers):
|
||||
matches = [(m(r1, r2), m) for m in matchers]
|
||||
_log_matches(r1, r2, matches)
|
||||
return all([m[0] for m in matches])
|
||||
return all(m[0] for m in matches)
|
||||
|
||||
@@ -59,7 +59,7 @@ def build_uri(**parts):
|
||||
port = parts['port']
|
||||
scheme = parts['protocol']
|
||||
default_port = {'https': 443, 'http': 80}[scheme]
|
||||
parts['port'] = ':{0}'.format(port) if port != default_port else ''
|
||||
parts['port'] = ':{}'.format(port) if port != default_port else ''
|
||||
return "{protocol}://{host}{port}{path}".format(**parts)
|
||||
|
||||
|
||||
@@ -161,8 +161,9 @@ def main():
|
||||
for file_path in files:
|
||||
migrated = try_migrate(file_path)
|
||||
status = 'OK' if migrated else 'FAIL'
|
||||
sys.stderr.write("[{0}] {1}\n".format(status, file_path))
|
||||
sys.stderr.write("[{}] {}\n".format(status, file_path))
|
||||
sys.stderr.write("Done.\n")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
||||
59
vcr/patch.py
59
vcr/patch.py
@@ -12,16 +12,6 @@ _HTTPConnection = httplib.HTTPConnection
|
||||
_HTTPSConnection = httplib.HTTPSConnection
|
||||
|
||||
|
||||
# Try to save the original types for requests
|
||||
try:
|
||||
import requests.packages.urllib3.connectionpool as cpool
|
||||
except ImportError: # pragma: no cover
|
||||
pass
|
||||
else:
|
||||
_VerifiedHTTPSConnection = cpool.VerifiedHTTPSConnection
|
||||
_cpoolHTTPConnection = cpool.HTTPConnection
|
||||
_cpoolHTTPSConnection = cpool.HTTPSConnection
|
||||
|
||||
# Try to save the original types for boto3
|
||||
try:
|
||||
import botocore.vendored.requests.packages.urllib3.connectionpool as cpool
|
||||
@@ -32,14 +22,27 @@ else:
|
||||
_cpoolBoto3HTTPConnection = cpool.HTTPConnection
|
||||
_cpoolBoto3HTTPSConnection = cpool.HTTPSConnection
|
||||
|
||||
|
||||
cpool = None
|
||||
# Try to save the original types for urllib3
|
||||
try:
|
||||
import urllib3
|
||||
import urllib3.connectionpool as cpool
|
||||
except ImportError: # pragma: no cover
|
||||
pass
|
||||
else:
|
||||
_VerifiedHTTPSConnection = urllib3.connectionpool.VerifiedHTTPSConnection
|
||||
_VerifiedHTTPSConnection = cpool.VerifiedHTTPSConnection
|
||||
_cpoolHTTPConnection = cpool.HTTPConnection
|
||||
_cpoolHTTPSConnection = cpool.HTTPSConnection
|
||||
|
||||
# Try to save the original types for requests
|
||||
try:
|
||||
if not cpool:
|
||||
import requests.packages.urllib3.connectionpool as cpool
|
||||
except ImportError: # pragma: no cover
|
||||
pass
|
||||
else:
|
||||
_VerifiedHTTPSConnection = cpool.VerifiedHTTPSConnection
|
||||
_cpoolHTTPConnection = cpool.HTTPConnection
|
||||
_cpoolHTTPSConnection = cpool.HTTPSConnection
|
||||
|
||||
|
||||
# Try to save the original types for httplib2
|
||||
@@ -166,7 +169,7 @@ class CassettePatcherBuilder(object):
|
||||
bases = (base_class,)
|
||||
if not issubclass(base_class, object): # Check for old style class
|
||||
bases += (object,)
|
||||
return type('{0}{1}'.format(base_class.__name__, self._cassette._path),
|
||||
return type('{}{}'.format(base_class.__name__, self._cassette._path),
|
||||
bases, dict(cassette=self._cassette))
|
||||
|
||||
@_build_patchers_from_mock_triples_decorator
|
||||
@@ -176,10 +179,9 @@ class CassettePatcherBuilder(object):
|
||||
|
||||
def _requests(self):
|
||||
try:
|
||||
import requests.packages.urllib3.connectionpool as cpool
|
||||
from .stubs import requests_stubs
|
||||
except ImportError: # pragma: no cover
|
||||
return ()
|
||||
from .stubs import requests_stubs
|
||||
return self._urllib3_patchers(cpool, requests_stubs)
|
||||
|
||||
def _boto3(self):
|
||||
@@ -301,7 +303,6 @@ class CassettePatcherBuilder(object):
|
||||
self._get_cassette_subclass(stubs.VCRRequestsHTTPSConnection)
|
||||
)
|
||||
mock_triples = (
|
||||
(cpool, 'VerifiedHTTPSConnection', stubs.VCRRequestsHTTPSConnection),
|
||||
(cpool, 'VerifiedHTTPSConnection', stubs.VCRRequestsHTTPSConnection),
|
||||
(cpool, 'HTTPConnection', stubs.VCRRequestsHTTPConnection),
|
||||
(cpool, 'HTTPSConnection', stubs.VCRRequestsHTTPSConnection),
|
||||
@@ -361,8 +362,22 @@ class ConnectionRemover(object):
|
||||
def reset_patchers():
|
||||
yield mock.patch.object(httplib, 'HTTPConnection', _HTTPConnection)
|
||||
yield mock.patch.object(httplib, 'HTTPSConnection', _HTTPSConnection)
|
||||
|
||||
try:
|
||||
import requests.packages.urllib3.connectionpool as cpool
|
||||
import requests
|
||||
if requests.__build__ < 0x021603:
|
||||
# Avoid double unmock if requests 2.16.3
|
||||
# First, this is pointless, requests.packages.urllib3 *IS* urllib3 (see packages.py)
|
||||
# Second, this is unmocking twice the same classes with different namespaces
|
||||
# and is creating weird issues and bugs:
|
||||
# > AssertionError: assert <class 'urllib3.connection.HTTPConnection'>
|
||||
# > is <class 'requests.packages.urllib3.connection.HTTPConnection'>
|
||||
# This assert should work!!!
|
||||
# Note that this also means that now, requests.packages is never imported
|
||||
# if requests 2.16.3 or greater is used with VCRPy.
|
||||
import requests.packages.urllib3.connectionpool as cpool
|
||||
else:
|
||||
raise ImportError("Skip requests not vendored anymore")
|
||||
except ImportError: # pragma: no cover
|
||||
pass
|
||||
else:
|
||||
@@ -385,11 +400,11 @@ def reset_patchers():
|
||||
pass
|
||||
else:
|
||||
yield mock.patch.object(cpool, 'VerifiedHTTPSConnection', _VerifiedHTTPSConnection)
|
||||
yield mock.patch.object(cpool, 'HTTPConnection', _HTTPConnection)
|
||||
yield mock.patch.object(cpool, 'HTTPSConnection', _HTTPSConnection)
|
||||
yield mock.patch.object(cpool, 'HTTPConnection', _cpoolHTTPConnection)
|
||||
yield mock.patch.object(cpool, 'HTTPSConnection', _cpoolHTTPSConnection)
|
||||
if hasattr(cpool.HTTPConnectionPool, 'ConnectionCls'):
|
||||
yield mock.patch.object(cpool.HTTPConnectionPool, 'ConnectionCls', _HTTPConnection)
|
||||
yield mock.patch.object(cpool.HTTPSConnectionPool, 'ConnectionCls', _HTTPSConnection)
|
||||
yield mock.patch.object(cpool.HTTPConnectionPool, 'ConnectionCls', _cpoolHTTPConnection)
|
||||
yield mock.patch.object(cpool.HTTPSConnectionPool, 'ConnectionCls', _cpoolHTTPSConnection)
|
||||
|
||||
try:
|
||||
import botocore.vendored.requests.packages.urllib3.connectionpool as cpool
|
||||
|
||||
@@ -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
|
||||
from ..serialize import serialize, deserialize
|
||||
|
||||
|
||||
class FilesystemPersister(object):
|
||||
|
||||
@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)
|
||||
if dirname and not os.path.exists(dirname):
|
||||
os.makedirs(dirname)
|
||||
|
||||
@@ -81,7 +81,7 @@ class Request(object):
|
||||
return self.scheme
|
||||
|
||||
def __str__(self):
|
||||
return "<Request ({0}) {1}>".format(self.method, self.uri)
|
||||
return "<Request ({}) {}>".format(self.method, self.uri)
|
||||
|
||||
def __repr__(self):
|
||||
return self.__str__()
|
||||
|
||||
@@ -24,7 +24,7 @@ def convert_body_to_bytes(resp):
|
||||
http://pyyaml.org/wiki/PyYAMLDocumentation#Python3support
|
||||
"""
|
||||
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')
|
||||
except (KeyError, TypeError, UnicodeEncodeError):
|
||||
# The thing we were converting either wasn't a dictionary or didn't
|
||||
|
||||
@@ -18,7 +18,7 @@ log = logging.getLogger(__name__)
|
||||
class VCRFakeSocket(object):
|
||||
"""
|
||||
A socket that doesn't do anything!
|
||||
Used when playing back casssettes, when there
|
||||
Used when playing back cassettes, when there
|
||||
is no actual open socket.
|
||||
"""
|
||||
|
||||
@@ -132,10 +132,13 @@ class VCRConnection(object):
|
||||
"""
|
||||
port = self.real_connection.port
|
||||
default_port = {'https': 443, 'http': 80}[self._protocol]
|
||||
return ':{0}'.format(port) if port != default_port else ''
|
||||
return ':{}'.format(port) if port != default_port else ''
|
||||
|
||||
def _uri(self, url):
|
||||
"""Returns request absolute URI"""
|
||||
if url and not url.startswith('/'):
|
||||
# Then this must be a proxy request.
|
||||
return url
|
||||
uri = "{0}://{1}{2}{3}".format(
|
||||
self._protocol,
|
||||
self.real_connection.host,
|
||||
@@ -146,14 +149,14 @@ class VCRConnection(object):
|
||||
|
||||
def _url(self, uri):
|
||||
"""Returns request selector url from absolute URI"""
|
||||
prefix = "{0}://{1}{2}".format(
|
||||
prefix = "{}://{}{}".format(
|
||||
self._protocol,
|
||||
self.real_connection.host,
|
||||
self._port_postfix(),
|
||||
)
|
||||
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'''
|
||||
self._vcr_request = Request(
|
||||
method=method,
|
||||
@@ -161,13 +164,15 @@ class VCRConnection(object):
|
||||
body=body,
|
||||
headers=headers or {}
|
||||
)
|
||||
log.debug('Got {0}'.format(self._vcr_request))
|
||||
log.debug('Got {}'.format(self._vcr_request))
|
||||
|
||||
# Note: The request may not actually be finished at this point, so
|
||||
# I'm not sending the actual request until getresponse(). This
|
||||
# allows me to compare the entire length of the response to see if it
|
||||
# exists in the cassette.
|
||||
|
||||
self._sock = VCRFakeSocket()
|
||||
|
||||
def putrequest(self, method, url, *args, **kwargs):
|
||||
"""
|
||||
httplib gives you more than one way to do it. This is a way
|
||||
@@ -180,7 +185,7 @@ class VCRConnection(object):
|
||||
body="",
|
||||
headers={}
|
||||
)
|
||||
log.debug('Got {0}'.format(self._vcr_request))
|
||||
log.debug('Got {}'.format(self._vcr_request))
|
||||
|
||||
def putheader(self, header, *values):
|
||||
self._vcr_request.headers[header] = values
|
||||
@@ -214,7 +219,7 @@ class VCRConnection(object):
|
||||
# then return it
|
||||
if self.cassette.can_play_response_for(self._vcr_request):
|
||||
log.info(
|
||||
"Playing response for {0} from cassette".format(
|
||||
"Playing response for {} from cassette".format(
|
||||
self._vcr_request
|
||||
)
|
||||
)
|
||||
@@ -236,7 +241,7 @@ class VCRConnection(object):
|
||||
# and return it.
|
||||
|
||||
log.info(
|
||||
"{0} not in cassette, sending to real server".format(
|
||||
"{} not in cassette, sending to real server".format(
|
||||
self._vcr_request
|
||||
)
|
||||
)
|
||||
@@ -287,13 +292,17 @@ class VCRConnection(object):
|
||||
# Cassette is write-protected, don't actually connect
|
||||
return
|
||||
|
||||
return self.real_connection.connect(*args, **kwargs)
|
||||
from vcr.patch import force_reset
|
||||
with force_reset():
|
||||
return self.real_connection.connect(*args, **kwargs)
|
||||
|
||||
self._sock = VCRFakeSocket()
|
||||
|
||||
@property
|
||||
def sock(self):
|
||||
if self.real_connection.sock:
|
||||
return self.real_connection.sock
|
||||
return VCRFakeSocket()
|
||||
return self._sock
|
||||
|
||||
@sock.setter
|
||||
def sock(self, value):
|
||||
@@ -311,6 +320,8 @@ class VCRConnection(object):
|
||||
with force_reset():
|
||||
self.real_connection = self._baseclass(*args, **kwargs)
|
||||
|
||||
self._sock = None
|
||||
|
||||
def __setattr__(self, name, value):
|
||||
"""
|
||||
We need to define this because any attributes that are set on the
|
||||
@@ -332,6 +343,23 @@ class VCRConnection(object):
|
||||
|
||||
super(VCRConnection, self).__setattr__(name, value)
|
||||
|
||||
def __getattr__(self, name):
|
||||
"""
|
||||
Send requests for weird attributes up to the real connection
|
||||
(counterpart to __setattr above)
|
||||
"""
|
||||
if self.__dict__.get('real_connection'):
|
||||
# check in case real_connection has not been set yet, such as when
|
||||
# we're setting the real_connection itself for the first time
|
||||
return getattr(self.real_connection, name)
|
||||
|
||||
return super(VCRConnection, self).__getattr__(name)
|
||||
|
||||
|
||||
for k, v in HTTPConnection.__dict__.items():
|
||||
if isinstance(v, staticmethod):
|
||||
setattr(VCRConnection, k, v)
|
||||
|
||||
|
||||
class VCRHTTPConnection(VCRConnection):
|
||||
'''A Mocked class for HTTP requests'''
|
||||
|
||||
@@ -6,59 +6,77 @@ 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))
|
||||
def __init__(self, method, url):
|
||||
super().__init__(
|
||||
method=method,
|
||||
url=url,
|
||||
writer=None,
|
||||
continue100=None,
|
||||
timer=None,
|
||||
request_info=None,
|
||||
traces=None,
|
||||
loop=asyncio.get_event_loop(),
|
||||
session=None,
|
||||
)
|
||||
|
||||
@asyncio.coroutine
|
||||
def text(self, encoding='utf-8'):
|
||||
return self.content.decode(encoding)
|
||||
async def json(self, *, encoding='utf-8', loads=json.loads, **kwargs): # NOQA: E999
|
||||
return loads(self._body.decode(encoding))
|
||||
|
||||
@asyncio.coroutine
|
||||
def release(self):
|
||||
async def text(self, encoding='utf-8'):
|
||||
return self._body.decode(encoding)
|
||||
|
||||
async def read(self):
|
||||
return self._body
|
||||
|
||||
async def release(self):
|
||||
pass
|
||||
|
||||
|
||||
def vcr_request(cassette, real_request):
|
||||
|
||||
@functools.wraps(real_request)
|
||||
@asyncio.coroutine
|
||||
def new_request(self, method, url, **kwargs):
|
||||
async def new_request(self, method, url, **kwargs):
|
||||
headers = kwargs.get('headers')
|
||||
headers = self._prepare_headers(headers)
|
||||
data = kwargs.get('data')
|
||||
params = kwargs.get('params')
|
||||
|
||||
vcr_request = Request(method, url, data, headers)
|
||||
request_url = URL(url)
|
||||
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, vcr_response.get('url'))
|
||||
response = MockClientResponse(method, URL(vcr_response.get('url')))
|
||||
response.status = vcr_response['status']['code']
|
||||
response.content = vcr_response['body']['string']
|
||||
response._body = vcr_response['body']['string']
|
||||
response.reason = vcr_response['status']['message']
|
||||
response.headers = vcr_response['headers']
|
||||
response._headers = vcr_response['headers']
|
||||
|
||||
response.close()
|
||||
return response
|
||||
|
||||
if cassette.write_protected and cassette.filter_request(vcr_request):
|
||||
response = MockClientResponse(method, url)
|
||||
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._body = msg.encode()
|
||||
response.close()
|
||||
return response
|
||||
|
||||
response = yield from real_request(self, method, url, **kwargs) # NOQA: E999
|
||||
response = await real_request(self, method, url, **kwargs) # NOQA: E999
|
||||
|
||||
vcr_response = {
|
||||
'status': {
|
||||
@@ -66,7 +84,7 @@ def vcr_request(cassette, real_request):
|
||||
'message': response.reason,
|
||||
},
|
||||
'headers': dict(response.headers),
|
||||
'body': {'string': (yield from response.text())}, # NOQA: E999
|
||||
'body': {'string': (await response.read())}, # NOQA: E999
|
||||
'url': response.url,
|
||||
}
|
||||
cassette.append(vcr_request, vcr_response)
|
||||
@@ -13,9 +13,7 @@ class VCRHTTPConnectionWithTimeout(VCRHTTPConnection,
|
||||
HTTPConnection.__init__.'''
|
||||
|
||||
# Delete the keyword arguments that HTTPConnection would not recognize
|
||||
safe_keys = set(
|
||||
('host', 'port', 'strict', 'timeout', 'source_address')
|
||||
)
|
||||
safe_keys = {'host', 'port', 'strict', 'timeout', 'source_address'}
|
||||
unknown_keys = set(kwargs.keys()) - safe_keys
|
||||
safe_kwargs = kwargs.copy()
|
||||
for kw in unknown_keys:
|
||||
@@ -33,7 +31,7 @@ class VCRHTTPSConnectionWithTimeout(VCRHTTPSConnection,
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
||||
# Delete the keyword arguments that HTTPSConnection would not recognize
|
||||
safe_keys = set((
|
||||
safe_keys = {
|
||||
'host',
|
||||
'port',
|
||||
'key_file',
|
||||
@@ -42,7 +40,8 @@ class VCRHTTPSConnectionWithTimeout(VCRHTTPSConnection,
|
||||
'timeout',
|
||||
'source_address',
|
||||
'ca_certs',
|
||||
))
|
||||
'disable_ssl_certificate_validation',
|
||||
}
|
||||
unknown_keys = set(kwargs.keys()) - safe_keys
|
||||
safe_kwargs = kwargs.copy()
|
||||
for kw in unknown_keys:
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
'''Stubs for requests'''
|
||||
|
||||
try:
|
||||
from requests.packages.urllib3.connectionpool import HTTPConnection, VerifiedHTTPSConnection
|
||||
except ImportError:
|
||||
from urllib3.connectionpool import HTTPConnection, VerifiedHTTPSConnection
|
||||
except ImportError:
|
||||
from requests.packages.urllib3.connectionpool import HTTPConnection, VerifiedHTTPSConnection
|
||||
|
||||
from ..stubs import VCRHTTPConnection, VCRHTTPSConnection
|
||||
|
||||
|
||||
Reference in New Issue
Block a user