1
0
mirror of https://github.com/kevin1024/vcrpy.git synced 2025-12-08 16:53:23 +00:00

Compare commits

...

441 Commits

Author SHA1 Message Date
Luiz Menezes
0cf11d4525 Bump version to 2.0.0 2018-09-19 11:46:08 -03:00
Luiz Menezes
75a334686f Merge pull request #391 from kevin1024/py37
Add support to py37
2018-09-18 19:40:45 -03:00
Luiz Menezes
9a9cdb3a95 add py37 on CI build 2018-09-18 19:04:30 -03:00
Luiz Menezes
b38915a89a Fix httplib2 compatibility with py37 2018-09-18 18:42:22 -03:00
Felix Yan
e93060c81b Fix compatibility with Python 3.7 2018-09-18 16:41:42 -03:00
Luiz Menezes
10736db427 Merge pull request #374 from adamchainz/readme_supported
Update docs' lists of supported HTTP clients
2018-09-18 14:45:11 -03:00
Luiz Menezes
cb4228cf90 Merge pull request #390 from kevin1024/fix-aiohttp-client
Fix vcr to support aiohttp client requests
2018-09-18 14:42:59 -03:00
Luiz Menezes
f7c051cde6 Drop support to asyncio.coroutine (py34 async/await syntax) 2018-09-18 14:14:25 -03:00
Luiz Menezes
075dde6707 Merge pull request #384 from valgur/patch-1
Fix PyPI badge URLs in README.rst
2018-09-18 12:55:32 -03:00
Luiz Menezes
af2742b6b9 Merge pull request #389 from lamenezes/fix-proxy
Fix proxy
2018-09-18 11:32:53 -03:00
Luiz Menezes
e9d00a5e2a Fix test_use_proxy cassette response type 2018-09-18 11:13:39 -03:00
Danilo Shiga
957db22d5c Improve test_use_proxy with cassette headers and play_count assertion 2018-09-18 11:06:15 -03:00
Martin Valgur
5ddcaa4870 Replace PNG badges with SVG ones 2018-09-05 20:26:01 +03:00
Martin Valgur
76076e5ccb Fix PyPI badge URLs in README.rst
`vcrpy-unittest` -> `vcrpy` in PyPI URLs.
2018-09-05 20:05:27 +03:00
Adam Johnson
7417978e36 tornado.httpclient 2018-07-31 10:38:21 +01:00
Stefan Tjarks
e559be758a Fix aiohttp patch to work with aiohttp >= 3.3
Aiohttp expects an awaitable instance to be returned from
`ClientSession._request` though `asyncio.coroutine` decorated function
do not implement `__await__`. By changing the syntax and dropping Python
3.4 support we fix this issue.
2018-07-25 13:24:29 -03:00
Adam Johnson
bb8d39dd20 Update docs' lists of supported HTTP clients
I noticed these were out of sync, especially weirdly one mentioning boto and the other mentioning boto3. I figure the list in the README is redundant when the Installation docs section lists the supported libraries, so I've removed it. I also:

* alphabetically sorted the list
* Highlighted the library names as code
* Added both `boto` and `boto3` to the list since there is support for both
* Removed the comment about Tornado's AsyncHTTPClient since that's an implementation detail plus the patch happens on a couple different classes
* Removed the note about `http.client` being part of Python 3, because everyone is Python 3 these days anyway :)
2018-07-18 22:51:58 +01:00
Samuel Fekete
ff7dd06f47 fix proxy for Python 2 2018-07-13 16:44:19 +01:00
Samuel Fekete
4b6b5effc7 Add headers in proxy server response 2018-07-13 16:44:19 +01:00
Samuel Fekete
06dc2190d6 Fix format string for Python 2.6 2018-07-13 16:40:35 +01:00
Samuel Fekete
eb4774a7d2 Only have a sock attribute after connecting 2018-07-13 16:40:35 +01:00
Samuel Fekete
bed9e520a3 Fix socketserver for Python 3 2018-07-13 16:40:35 +01:00
Samuel Fekete
365a98bf66 Fix failing tests 2018-07-13 16:40:35 +01:00
Samuel Fekete
fc95e34bd4 Determine proxy based on path 2018-07-13 16:40:35 +01:00
Samuel Fekete
236dc1f4f2 Add test for proxies 2018-07-13 16:34:21 +01:00
Samuel Fekete
43f4eb8156 Fix host and port for proxy connections 2018-07-13 16:34:21 +01:00
Luiz Menezes
aff71c5107 Merge pull request #356 from kgraves/fix_before_record_response_mutates_actual_response
fixes before_record_response mutates response
2018-07-12 23:19:57 -03:00
Luiz Menezes
506700651d Bump version to 1.13.0 2018-07-12 20:07:30 -03:00
Luiz Menezes
d1b11da610 Merge pull request #369 from lamenezes/fix-aiohttp-url-with-query-params
Fix aiohttp url with query params
2018-07-08 23:48:20 -03:00
Luiz Menezes
306238d561 Test aiohttp usage with query strings on the URL 2018-07-08 23:06:22 -03:00
Goran Stefkovski
dbddaa0e44 Shallow copy of query as mutable MultiDict 2018-07-08 23:04:36 -03:00
Goran Stefkovski
0d4c9eccf5 simplified logic so that either params or url is used, if params are specified - they will overwrite any get params on the url 2018-07-08 23:03:42 -03:00
Luiz Menezes
1674741d9f Merge pull request #368 from lamenezes/fix-json-content-type-on-aiohttp-stub
Fix content type being passed to aiohttp response stub
2018-07-08 14:46:16 -03:00
Luiz Menezes
75cb067e29 Fix content type being passed to aiohttp response stub 2018-07-07 23:56:39 -03:00
Luiz Menezes
ab6e6b5b5d Merge pull request #359 from lamenezes/adapt-aiohttp-stub-to-3.3
Fix aiohttp stub to work with aiohttp 3.3.x
2018-06-18 10:46:36 -03:00
Luiz Menezes
9e8bd382d3 Fix aiohttp stub to work with aiohttp 3.3.x 2018-06-05 11:59:28 -03:00
kg
ba79174a1f fixes before_record_response mutates response
When no cassette exists, it's expected that the response returned, should be
the original, unchanged response. The response recorded in the cassette should be
that which is returned by the before_record_response callback.

But on subsequent requests/responses (when a cassette exists), the responses
returned should be exactly what is in the cassette.

resolves #355
2018-05-24 01:12:13 +00:00
Kevin McCarthy
c341e48961 bump version for release 2018-05-21 08:57:42 -05:00
Luiz Menezes
5be75692c4 Merge pull request #353 from lamenezes/support-aiohttp-over-3.1.0
Fix aiohttp stub to support version >= 3.1.0
2018-05-16 15:13:37 -03:00
Luiz Menezes
b10b92bdbb Fix travis config to allow pypy3.5-aiohttp failure instead of excluding 2018-05-16 14:09:39 -03:00
Luiz Menezes
3009cbbbe9 Skip requests test instead of failing for python >= 3.6 2018-05-16 12:39:44 -03:00
Luiz Menezes
f811b41ad9 Fix tests aiohttp_utils.aiohttp_request return values 2018-05-16 11:12:51 -03:00
Luiz Menezes
140bc2ee74 Allow test failure on pypy3+aiohttp 2018-05-08 09:45:17 -03:00
Luiz Menezes
867fd9ab4b Ignore flake8 for entire file on aiohttp_utils 2018-05-07 10:58:09 -03:00
Luiz Menezes
545c903ee2 pep8 fix on aiohttp_utils 2018-05-07 10:00:27 -03:00
Luiz Menezes
cd864b5eca Fix aiohttp tests to include content type when parsing response to json 2018-05-07 09:55:12 -03:00
Luiz Menezes
689d68a0a2 Use async syntax on aiohttp_utils 2018-05-07 09:25:21 -03:00
Luiz Menezes
709017ea46 Fix aiohttp utils to pass encondig to response.json 2018-05-07 09:25:21 -03:00
Luiz Menezes
8621427f46 Add .pytest_cache/ to .gitignore 2018-05-07 09:25:21 -03:00
Luiz Menezes
7e695ff7bc Fix test aiohttp imports 2018-05-07 09:25:21 -03:00
Luiz Menezes
bd08e5119f Maintain support to python 3.4 and aiohttp 2018-05-07 09:25:21 -03:00
Luiz Menezes
6ab508d67d Fix aiohttp_request to properly perform aiohttp requests 2018-05-07 09:25:21 -03:00
Luiz Menezes
f1561ae0f8 Remove tox pin on aiohttp 2018-05-07 09:25:21 -03:00
Luiz Menezes
f1f8ce2af4 Ignore syntax error on async stuff 2018-05-07 09:25:21 -03:00
Luiz Menezes
26be756f47 Fix aiohttp stub to support version >= 3.1.0 2018-05-07 09:25:21 -03:00
Luiz Menezes
f890709a20 Merge pull request #346 from carsonyl/patch-1
Convert extras_require conditional deps to PEP 508 form.
2018-05-06 19:06:06 -03:00
Carson Lam
d0ae5fa40b Merge branch 'master' of github.com:kevin1024/vcrpy into patch-1 2018-05-05 19:53:42 -07:00
Luiz Menezes
1562bc7659 Merge pull request #350 from lamby/895269-vcrpy-please-make-the-build-reproducible
Make the build reproducible
2018-05-03 12:31:29 -03:00
Luiz Menezes
16b69aa2e5 Merge pull request #320 from allisson/master
Update aiohttp_stub to work with binary content
2018-05-03 12:30:41 -03:00
Allisson Azevedo
d9caff107d Merge remote-tracking branch 'upstream/master' 2018-05-02 15:03:01 -03:00
Chris Lamb
f317490eec Make the build reproducible
Whilst working on the Reproducible Builds effort [0], we noticed
that vcrpy could not be built reproducibly.

This is due to the documentation including the absolute build path
via Python default arguments.

This was originally filed in Debian as #895269 [1].

 [0] https://reproducible-builds.org/
 [1] https://bugs.debian.org/895269

Signed-off-by: Chris Lamb <lamby@debian.org>
2018-05-02 10:14:43 -07:00
Luiz Menezes
cf13805973 Merge pull request #354 from kevin1024/build-quicker
Fix broken tests + build quicker
2018-05-02 13:31:36 -03:00
Luiz Menezes
389cb4d6e3 Temporarily pins aiohttp to version <3 2018-05-02 12:19:55 -03:00
Luiz Menezes
7a82d70391 Pin flask version (as latests 1.0 breaks retrocompatibility) 2018-05-02 11:59:01 -03:00
Carson Lam
f3b9966a2a Pass flake8. 2018-02-21 23:16:29 -08:00
Carson Lam
5ba1c7fbb6 Convert extras_require conditional deps to PEP 508 form. 2018-02-21 21:40:02 -08:00
Allisson Azevedo
ad153bd733 Merge remote-tracking branch 'upstream/master' 2018-02-17 11:51:04 -03:00
Hugo
42b3b16fe1 Remove boto 2018-01-19 23:56:06 +02:00
Hugo
531dc02ca5 Only test on single recent version of dependencies 2018-01-17 10:28:36 +02:00
Thomas Grainger
2156adb841 Merge pull request #340 from hugovk/rm-2.6
Drop support for EOL Python 2.6 and 3.3
2018-01-16 17:23:12 +00:00
Hugo
6caf7e962e Requests 1.x only supports Python <= 3.3 2018-01-15 22:37:27 +02:00
Hugo
97fbd7e0bd Test flakes on Python 2.7 (and 3.6) not pypy3.5 2018-01-15 22:36:19 +02:00
Hugo
ead48b1907 xfail for test_post_chunked_binary_secure on CPython 3.5 2018-01-15 15:05:53 +02:00
Hugo
1af4b2587e Add pypy3.5-5.9.0 2018-01-15 10:04:25 +02:00
Hugo
82fa50c092 Drop pypy3 2018-01-15 09:54:39 +02:00
Hugo
58d8980cfa Remove dependency on bugs.python.org/xmlrpc: 301 Moved Permanently 2018-01-15 09:54:11 +02:00
Hugo
c111ebab0a Remove dependency on old Flask 2018-01-15 09:54:11 +02:00
Hugo
943a15a967 Drop support for EOL Python 3.3 2018-01-15 09:54:11 +02:00
Hugo
d0aa6bcc8d Rewrite unnecessary list/tuple literals as set literals 2018-01-15 09:54:11 +02:00
Hugo
04fd730a08 Replace unnecessary list comprehension - 'all' can take a generator 2018-01-15 09:54:11 +02:00
Hugo
6156271c48 Automatic formatters supported in Python 2.7+ 2018-01-15 09:54:11 +02:00
Hugo
87666ba2e4 Multiple context managers supported in 2.7+ 2018-01-15 09:54:11 +02:00
Hugo
7915d07aff Update classifiers 2018-01-15 09:54:11 +02:00
Hugo
095e272191 Add python_requires to help pip install correct version 2018-01-15 09:54:11 +02:00
Hugo
42762ec806 collections.Counter is new in Python 2.7 2018-01-15 09:54:11 +02:00
Hugo
bfb38af8e1 Drop support for EOL Python 2.6 2018-01-15 09:54:11 +02:00
Hugo
894695d13b Stop testing on EOL Python 2.6 2018-01-15 09:54:11 +02:00
Andrew Kofink
a56a0726d4 Correct libyaml-devel package name in EL/Fedora 2017-10-30 10:20:56 -10:00
Ivan Malison
c366852925 Merge pull request #324 from petr-bulusek/custom-persister
Fix of registering custom persister and test.
2017-10-25 02:17:08 -07:00
Allisson Azevedo
0cab15658f Merge remote-tracking branch 'upstream/master' 2017-09-20 11:58:07 -03:00
Ivan Malison
c3ecf8c5b2 Merge pull request #326 from lmazuel/master
Fix mixup between httplib and urllib3
2017-08-17 10:08:38 -07:00
Laurent Mazuel
81d453f7d3 Add requests 2.18.4 to build 2017-08-17 08:37:11 -07:00
Laurent Mazuel
262ad903cb Don't unmock requests.packages if not necessary 2017-08-17 08:37:11 -07:00
Laurent Mazuel
ec60af0214 Fix mixup between httplib and urllib3 2017-08-17 08:37:11 -07:00
Petr Bulusek
8cf8d3f69c delete ipdb.set_trace 2017-08-12 17:58:52 +02:00
Petr Bulusek
034aeb4f17 typo 2017-08-11 14:41:11 +02:00
Petr Bulusek
d59efbc6e0 Instantiating class not necessary. 2017-08-09 15:18:00 +02:00
Petr Bulusek
b753a491c9 revert PR unrelated change, better comment 2017-08-09 14:44:39 +02:00
Petr Bulusek
9092b34dd1 register custom persister fix 2017-08-09 14:25:10 +02:00
Mikaeil Orfanian
0a3aaddca2 Update usage.rst
"i.e." is misleading because it means "in other words". In the context of this paragraph, "i.e." implies that "unexpected requests" are those that have a different URI.
I think a URI change is just one example of what could constitute an "unexpected" request. vcr does request matching based on method, port, body, etc.
So, I think "e.g." which means "for example" is the right phrase in this context.
2017-07-13 17:09:34 -05:00
Allisson Azevedo
c55d976277 Update aiohttp_stub to work with binary content 2017-06-22 15:12:58 -03:00
Ivan Malison
47ccddafee Merge pull request #314 from kevin1024/support-requests211
support requests 2.11 Fixes #313
2017-06-07 11:27:26 -07:00
Thomas Grainger
dcaf813657 support requests 2.11 Fixes #313 2017-06-07 14:10:23 +01:00
Kevin McCarthy
ef727aaaaf bump version 2017-05-27 16:47:11 -10:00
Kevin McCarthy
ee17233aa0 only need to run flakes on one version of python 2017-05-27 16:00:11 -10:00
Kevin McCarthy
f88294a9e6 Drop a couple old versions of python
According to the devguide: https://docs.python.org/devguide/

Python 3.3 is officially supported until 2017-09-29 and 3.4 until
2019-03-16

But I'm tired of waiting for my tests to run on Travis :-P
2017-05-27 15:52:59 -10:00
Kevin McCarthy
572da2084d Fix pyflake issue 2017-05-27 15:43:24 -10:00
Kevin McCarthy
88bf8f0aac proxy getattr to the real connection too 2017-05-27 15:40:42 -10:00
Kevin McCarthy
9b59e02374 Fix compat with requests 2.16 (unvendored urllib3)
The new version of requests un-vendors urllib3, with a nifty hack:
https://github.com/kennethreitz/requests/blob/master/requests/packages.py

Unfortunately messing directly with sys.modules causes some weird
behavior that I don't entirely understand.  Avoiding the extra import to
requests.packages as part of VCR's initialization seems to sidestep the
issue.

Closes #311
2017-05-27 15:40:38 -10:00
Kevin McCarthy
ba290a32d2 Add new version of urllib3 to tox
Also remove ancient version in the interest of test speed
2017-05-27 15:38:42 -10:00
Kevin McCarthy
420c2ceb6f add new requests version 2017-05-27 10:57:41 -10:00
Kevin McCarthy
ec786f2fd9 pass all args to runtests 2017-05-27 10:57:25 -10:00
Thomas Grainger
0c4020df7d version bump 1.11.0 2017-05-02 11:36:16 +01:00
Thomas Grainger
204cb8f2ac Merge pull request #303 from graingert/support-3.6
support 3.6
2017-05-02 11:17:39 +01:00
Thomas Grainger
0e421b5327 Merge pull request #301 from graingert/handle-pytest-asyncio-coroutines
handle pytest-asyncio async def coroutines
2017-05-02 11:17:06 +01:00
Thomas Grainger
dc2dc306d5 pytest-httpbin doesn't support chunked requests on Python 3.6 2017-04-04 11:30:16 +01:00
Thomas Grainger
1092bcd1a1 add requests 2.13 2017-04-04 10:32:17 +01:00
Thomas Grainger
73dbc6f8cb add missing _get_content_length static method
also add _is_textIO
2017-04-04 10:32:17 +01:00
Thomas Grainger
3e9fb10c11 Merge remote-tracking branch 'derekbekoe/fix-vcrconnection-3.6' into support-3.6 2017-04-04 10:32:17 +01:00
Thomas Grainger
3588ed6341 support 3.6 2017-04-04 10:32:16 +01:00
Kevin McCarthy
26326c3ef0 Merge pull request #305 from graingert/add-cache-to-gitignore
add .cache to gitignore
2017-04-03 06:40:21 -10:00
Thomas Grainger
7514d94262 handle pytest-asyncio async def coroutines 2017-04-03 15:49:49 +01:00
Thomas Grainger
1df577f0fc add .cache to gitignore 2017-04-03 15:45:42 +01:00
Kevin McCarthy
70f4707063 Merge pull request #302 from AartGoossens/feature/before_record_docs_fix
Improves docs for before_record_request
2017-03-16 08:55:34 -10:00
Aart Goossens
521146d64e Improves docs for before_record_request 2017-03-16 19:25:16 +01:00
Derek Bekoe
091b402594 Revert "Add Python 3.6 to CI"
This reverts commit 24b617a427.
2017-01-24 16:26:02 -08:00
Derek Bekoe
24b617a427 Add Python 3.6 to CI 2017-01-24 13:57:37 -08:00
Derek Bekoe
97473bb8d8 Correctly patch HTTPConnection.request in Python 3.6
Fixes https://github.com/kevin1024/vcrpy/issues/293
2017-01-23 14:54:26 -08:00
Kevin McCarthy
ed35643c3e Merge pull request #292 from j-funk/master
Allow injection of persistence methods
2017-01-22 08:29:52 -06:00
Julien Funk
2fb3b52c7e add custom persister docs 2017-01-19 13:10:27 -05:00
Julien Funk
9e70993d57 substiture IOError with more appropriate ValueError 2017-01-19 13:10:08 -05:00
Julien Funk
6887e2cff9 remove unused imports 2017-01-15 15:04:20 -08:00
Julien Funk
ba38680402 Merge pull request #1 from IvanMalison/persistence_methods
Fix patch of FliesystemPersister.load_cassette
2017-01-14 15:59:13 -05:00
Ivan Malison
06b00837fc Fix patch of FliesystemPersister.load_cassette 2017-01-13 15:29:45 -08:00
Julien Funk
a033bc729c refactored, 1 failing test 2017-01-13 16:09:42 -05:00
Julien Funk
6f8486e0a2 allow injection of persistence methods 2017-01-12 16:41:26 -05:00
Kevin McCarthy
53c55b13e7 version bump 2017-01-11 17:54:16 -10:00
MAA
365e7cb112 Removed duplicate mock triple. 2017-01-11 17:54:15 -10:00
Charly
e5d6327de9 added a fix to httplib2 2017-01-11 17:54:15 -10:00
Luiz Menezes
d86ffe7130 Add missing requirement yarl for python >= 3.4 2017-01-06 10:43:40 -02:00
Kevin McCarthy
d9fd563812 bump version 2016-12-15 08:47:01 -10:00
Kevin McCarthy
9e548718e5 fix whitespace 2016-12-15 08:47:01 -10:00
Kevin McCarthy
83720793fb Merge pull request #280 from madninja/fix_aiohttp
Fix up to support aiohttp 1.x
2016-11-08 10:15:07 -10:00
Marc Nijdam
188326b10e Fix flake errors 2016-11-07 12:03:21 -08:00
Marc Nijdam
ff90190660 Fix up to support aiohttp 1.x 2016-11-07 10:07:08 -08:00
Kevin McCarthy
1d9f8b5f7c bump version to 1.10.3 2016-10-02 12:15:12 -10:00
Kevin McCarthy
2454aa2eb0 Merge pull request #278 from kevin1024/empty_response_body
Empty response body
2016-10-02 12:10:04 -10:00
Kevin McCarthy
df5f6089af Merge pull request #279 from kevin1024/fix_nonetype_encode_exception
Fix nonetype encode exception
2016-10-02 12:09:55 -10:00
Kevin McCarthy
5738547288 Merge pull request #277 from kevin1024/fix_asyncio
Fix asyncio
2016-10-02 12:09:48 -10:00
Kevin McCarthy
8274b660c6 fix flake8 failure 2016-10-02 12:08:02 -10:00
Gregory Roussac
a8f1a65d62 test serializers.compat.convert_to_bytes() 2016-10-02 12:07:37 -10:00
Gregory Roussac
9c275dd86a VCR AttributeError: 'NoneType' object has no attribute 'encode'
Hi,

Using an old fork but may be usefull.

I think checking the string is required like on line 47.

Best regards
2016-10-02 12:07:37 -10:00
Kevin McCarthy
1fbd65a702 add test from @mbachry 2016-10-02 10:24:38 -10:00
Janez Troha
31b0e825b5 Handle empty body 2016-10-02 10:24:38 -10:00
Luiz Menezes
973d8339b3 add tests for aiohttp params fix 2016-10-02 10:22:29 -10:00
Alexander Novikov
c8db6cb731 Fix missing query string while params are passed in inside params argument 2016-10-02 10:22:29 -10:00
Kevin McCarthy
ecbc192fc4 bump version 2016-09-13 15:49:18 -10:00
Kevin McCarthy
76d365314a bump version 2016-09-11 18:02:37 -10:00
Kevin McCarthy
830a3c2e04 Merge pull request #272 from puiterwijk/fix-270
Move vcr.stubs.aiohttp_stub to a package
2016-09-09 11:54:08 -10:00
Patrick Uiterwijk
9c432c7e50 Move vcr.stubs.aiohttp_stub to a package
find_packages(exclude=) only works with packages, not modules.
So this fixes install_lib for python2 by correctly excluding that module.

Fixes: #270
Fixes: #271
Signed-off-by: Patrick Uiterwijk <puiterwijk@redhat.com>
2016-09-09 20:45:16 +00:00
Kevin McCarthy
6f7f45d0a8 Merge pull request #271 from lamenezes/fix-py2-setup
Exclude aiohttp from python < 3 setup
2016-09-09 06:38:10 -10:00
Luiz Menezes
8e352feb6a Exclude aiohttp from python < 3 setup 2016-08-31 22:15:24 -03:00
Kevin McCarthy
57a934d14b version bump to 1.10.0 2016-08-14 10:41:25 -10:00
Kevin McCarthy
f9d7ccd33e Merge pull request #266 from lamenezes/aiohttp-support
Aiohttp support
2016-08-14 10:37:14 -10:00
Luiz Menezes
265a158fe7 remove py26-flakes test 2016-08-12 13:50:29 -03:00
Luiz Menezes
c65ff0e7b3 fix flake8: ignore yield from syntax errors 2016-08-11 19:48:40 -03:00
Luiz Menezes
066752aa0b rename file 2016-08-11 08:32:47 -03:00
Luiz Menezes
9a5214888b fix tox's flakes tests 2016-08-11 00:58:18 -03:00
Luiz Menezes
609d8e35be fix test_aiohttp 2016-08-10 18:12:38 -03:00
Luiz Menezes
ce14de8251 fix tests 2016-08-10 15:56:19 -03:00
Luiz Menezes
574b22a62a remove async/await from aiohttp_stubs to support python 3.4 2016-08-10 15:51:11 -03:00
Luiz Menezes
1167b9ea4e fix .travis.yml 2016-08-04 14:03:42 -03:00
Luiz Menezes
77ae99bfda add aiohttp to tests config 2016-08-04 13:44:11 -03:00
Luiz Menezes
8851571ba7 add integration tests for aiohttp 2016-08-04 13:40:04 -03:00
Luiz Menezes
f71d28d10e fix aiohttp_stubs.vcr_request error message 2016-08-04 13:39:46 -03:00
Luiz Menezes
3355bd01eb fix aiohttp response closing 2016-08-04 13:39:09 -03:00
Luiz Menezes
17afa82bf4 remove CIMultiDictProxy from aiohttp_stubs.vcr_request 2016-08-04 13:37:55 -03:00
Luiz Menezes
f98684e8aa add support for aiohttp 2016-08-04 00:21:49 -03:00
Kevin McCarthy
5a85e88a39 Merge pull request #265 from adamchainz/readthedocs.io
Convert readthedocs links for their .org -> .io migration for hosted projects
2016-07-16 09:08:29 -10:00
Kevin McCarthy
d2368eb2c4 fix flaky test 2016-07-16 08:58:07 -10:00
Kevin McCarthy
3a46616ba6 bump version to 1.9.0 2016-07-16 08:07:51 -10:00
Adam Chainz
37665581e0 Convert readthedocs links for their .org -> .io migration for hosted projects
As per [their blog post of the 27th April](https://blog.readthedocs.com/securing-subdomains/) ‘Securing subdomains’:

> Starting today, Read the Docs will start hosting projects from subdomains on the domain readthedocs.io, instead of on readthedocs.org. This change addresses some security concerns around site cookies while hosting user generated data on the same domain as our dashboard.

Test Plan: Manually visited all the links I’ve modified.
2016-07-13 23:24:04 +01:00
Kevin McCarthy
57df0c6921 unzip bodies before comparing. Fixes #261 2016-07-03 17:27:52 -10:00
Kevin McCarthy
ddb29745a9 Merge pull request #247 from dedsm/boto3_support
Adding support for boto3
2016-07-02 14:00:02 -10:00
David de Sousa
ac7c9244cc Running boto3 tests in travis 2016-06-27 01:39:49 +02:00
David de Sousa
6da7cd0ea5 Fixing pep8 errors in boto tests 2016-06-27 01:39:49 +02:00
bogdan barna
24df79b75f boto3 integration tests 2016-06-27 01:39:49 +02:00
David de Sousa
0800b99214 Adding support for boto3 2016-06-27 01:39:49 +02:00
Kevin McCarthy
3dad89df3f Merge pull request #260 from foobarna/fix-tests
fix tests in stubs, requests ssl verification and httpbin+flask
2016-06-24 06:48:51 -10:00
bogdan barna
5c9b0b4ccb fix tests in stubs, requests ssl verification and httpbin+flask 2016-06-24 11:43:01 +03:00
Kevin McCarthy
5a848d277e Merge pull request #259 from nickdirienzo/master
Fix deepcopy issue for response headers when `decode_compressed_response` is enabled
2016-06-21 20:45:27 -10:00
Nick DiRienzo
c88c738df9 Removed requests usage from test 2016-06-21 07:14:51 -07:00
Nick DiRienzo
9a8067d8e7 Renamed inside2 to inside 2016-06-21 07:00:53 -07:00
Nick DiRienzo
787c6bdb77 Fix flake8 issue 2016-06-21 06:54:05 -07:00
Nick DiRienzo
c3298c25a3 Updated comments 2016-06-20 23:43:47 -07:00
Nick DiRienzo
2f4c803678 Added a note on the deepcopy 2016-06-20 23:36:51 -07:00
Nick DiRienzo
60145983bf Added regression test 2016-06-20 23:23:31 -07:00
Nick DiRienzo
b5c27f99d1 Move deepcopy higher to not mutate original headers 2016-06-20 16:57:40 -07:00
Ivan Malison
1ef099a13e v 1.8.0 2016-06-09 12:16:14 -07:00
Kevin McCarthy
34d07406f9 missed a httpbin call 2016-05-01 17:16:05 -10:00
Kevin McCarthy
e269c77670 Merge pull request #253 from jayvdb/no-pytest-localserver
Remove pytest-localserver from test dependencies
2016-05-01 16:43:09 -10:00
John Vandenberg
889edccecb Remove pytest-localserver from test dependencies
pytest-localserver is no longer needed after
the switch to using pytest-httpbin.
2016-05-02 09:23:06 +07:00
Kevin McCarthy
37c8cbca91 pep8 2016-05-01 14:44:13 -10:00
Aliaksandr Buhayeu
9daf301deb Fix for Serialization errors with JSON adapter
This patch aims to fix the issue#222,
where json data in request can not be serialized
because of TypeError in py3
2016-05-01 14:22:17 -10:00
Kevin McCarthy
528c9e7b1a Merge pull request #149 from kevin1024/pytest-httpbin
WIP: add pytest-httpbin
2016-05-01 14:19:54 -10:00
Kevin McCarthy
4e36997e1a Use pytest-httpbin
This will help the test flakiness and speed up test runs.
2016-05-01 13:50:04 -10:00
Kevin McCarthy
c571c932c9 Merge pull request #252 from jayvdb/travis-more-versions
Add Python 3.5 and PyPy 3 to Travis
2016-05-01 07:20:48 -10:00
John Vandenberg
d060a68ffd Add Python 3.5 and PyPy 3 to Travis 2016-05-01 07:54:59 +07:00
Kevin McCarthy
cfc483a08d Merge pull request #244 from jaysonsantos/master
Avoid concatenating bytes with strings
2016-04-26 08:59:07 -04:00
Jayson Reis
632af2e41a Fix/ignore some flake errors 2016-01-20 12:28:46 +01:00
Jayson Reis
7fdfce65ee Add test to make sure we can post chunked binary data 2016-01-20 12:17:28 +01:00
Jayson Reis
7cc513e1d2 Avoid concatenating bytes with strings 2016-01-19 14:19:03 +01:00
Ivan 'Goat' Malison
4f3c5c0a6e Merge pull request #243 from kevin1024/response_filtering
allow filtering by response
2016-01-11 17:12:44 -08:00
Ivan Malison
43b3411e6c Fix travis check for flakes 2016-01-11 17:00:02 -08:00
Ivan Malison
99d4150df8 allow filtering by response 2016-01-11 16:07:44 -08:00
Kevin McCarthy
8d5993eced Merge pull request #242 from koobs/patch-1
Exclude __pycache__ dirs & compiled files in sdist
2016-01-10 07:35:20 -10:00
Kubilay Kocak
8a1b7c6532 Exclude __pycache__ dirs & compiled files in sdist
The current vcrpy sdist on PyPI includes __pycache__ dirs and compiled files which causes tests to fail when running tests via setup.py test

import file mismatch:
imported module 'test_persist' has this __file__ attribute:
  /Users/imalison/Projects/vcrpy/tests/unit/test_persist.py
which is not the same as the test file we want to collect:
  /usr/home/user/repos/freebsd/ports/devel/py-vcrpy/work/vcrpy-1.7.4/tests/unit/test_persist.py
HINT: remove __pycache__ / .pyc files and/or use a unique basename for your test file modules

This change recursively excludes these directories and files from future source distributions
2016-01-11 02:27:41 +11:00
Ivan 'Goat' Malison
3459d95d4f Merge pull request #234 from tobiowo/decode-compressed-response
Decode compressed response option
2015-12-31 10:18:34 -08:00
Ivan 'Goat' Malison
ebaae9bed7 Merge pull request #240 from abhinav/old-tornado
Fix crashing with Tornado 3
2015-12-31 10:17:01 -08:00
Abhinav Gupta
d780bc04dd Fix Tornado support behavior for Tornado 3.
Resolves #235.
2015-12-19 18:18:20 -08:00
Kevin McCarthy
31c358c035 Merge pull request #238 from jayvdb/tox-travis
Use tox-travis
2015-12-12 10:51:07 -10:00
John Vandenberg
573c6eee0b Use tox-travis
dc9cd42 introduced Travis checking of flakes using tox.
However tox.ini only defined 'flakes-py34', so Travis
was only invoking flake8 on Python 3.4, and invoking
py.test on other Python versions.

As tox factors only work properly on the default testenv,
use a generic tox environment 'flakes', and use tox-travis
to select the correct basepython.
2015-12-13 04:22:51 +11:00
Kevin McCarthy
70c92d05d9 Merge pull request #229 from jayvdb/flakes
Fix pyflakes and pep8 errors
2015-12-05 14:06:53 -10:00
Olutobi Owoputi
5d866dd77c support python 3.4 2015-12-02 14:43:06 -08:00
Olutobi Owoputi
2d08358b5c tests / docs for decode_compressed_response 2015-12-02 12:26:23 -08:00
Olutobi Owoputi
64397d7ecc add decode_compressed_response option and filter 2015-12-02 12:25:36 -08:00
John Vandenberg
dc9cd4229b Fix pyflakes and pep8 errors
Use extra asserts to use previously unused variables in tests,
such as `cass` and `response`.

Fix only pyflakes errors in docs/conf.py
2015-11-26 08:25:06 +11:00
Kevin McCarthy
6ae1b00207 Merge pull request #228 from jayvdb/travis-tox
Use tox for Travis-CI
2015-11-25 09:23:28 -05:00
John Vandenberg
54bb9aa27a Use tox for Travis-CI 2015-11-25 15:51:25 +11:00
Aron Griffis
312ed2c234 Merge pull request #223 from jayvdb/jessie-fix
Fallback to importing from urllib3
2015-11-24 15:14:09 -05:00
Kevin McCarthy
20915a79c1 Merge pull request #226 from Bjwebb/patch-1
Fix typo
2015-11-14 07:22:33 -06:00
Ben Webb
495afdddc8 Fix typo 2015-11-14 07:41:38 +00:00
Aron Griffis
dee580f971 Merge pull request #196 from agriffis/remove-replace
Enable header replacement rather than removal
2015-11-07 19:01:00 -05:00
Aron Griffis
6919c06b8c Add documentation for new features of filter_headers, filter_query_parameters and filter_post_data_parameters 2015-11-07 18:50:32 -05:00
Aron Griffis
77de8dc47e Update VCR params to use new filters. 2015-11-07 17:05:25 -05:00
Aron Griffis
cb40a45eba Add replace_post_data_parameters() 2015-11-07 17:05:25 -05:00
Aron Griffis
678586904b Add replace_query_parameters() 2015-11-07 17:05:25 -05:00
Aron Griffis
ddbf0464f4 Add replace_headers() 2015-11-07 17:05:25 -05:00
Aron Griffis
e14b94789b Add note and link for vcrpy-unittest. 2015-11-07 17:04:56 -05:00
Aron Griffis
e6dba270ec Blindly add modules to be documented 2015-11-07 16:58:10 -05:00
Aron Griffis
615cf8661a Add PyPI badge. 2015-11-07 16:47:15 -05:00
Aron Griffis
ce6656c4d5 Rename waffle badge from 'ready' to 'waffle' which seems clearer. 2015-11-07 16:47:06 -05:00
Aron Griffis
8d083ba578 Split README.rst into appropriate docs sections 2015-11-07 16:38:02 -05:00
Aron Griffis
f0f5334c40 Use sphinx_rtd_theme building docs locally 2015-11-07 16:37:16 -05:00
Aron Griffis
8de2312ccc Switch to default RtD theme 2015-11-07 15:26:22 -05:00
Aron Griffis
c3f5ae84b1 Bump docs version to match project version 1.7.4 2015-11-07 15:23:14 -05:00
Ivan Malison
f6b8e4f8e7 Lint cleanup 2015-10-25 20:30:33 -07:00
Ivan Malison
2ac3fa9abe v1.7.4 2015-10-18 16:48:27 -07:00
John Vandenberg
dd8b39b29e Fallback to importing from urllib3
requests.packages.urllib3 may be literally urllib3
instead of vendored urllib3.
2015-10-15 14:56:30 +11:00
Ivan 'Goat' Malison
1324feae99 Merge pull request #217 from bcen/fix-decoration-no-return
Fix use_cassette when used as decorator does not return the wrapped function's return value
2015-10-02 00:11:20 -07:00
Bocai Cen
7990c549d1 fix decoration when in _handle_function does not return the wrapped function return value 2015-10-01 23:17:15 -04:00
Ivan 'Goat' Malison
327797c4ff Merge pull request #206 from tyewang/propogate_attribute_changes_to_real_connection_on_stubs
Attributes set on VCRConnection now also get set on the real_connection
2015-09-20 00:56:54 -07:00
Tye Wang
ac510097e0 Add TODO and elaborate on comment 2015-09-18 12:19:17 -04:00
Ivan Malison
00d973a0f5 sphinx skeleton 2015-09-13 11:27:08 -07:00
Tye Wang
79ff59feae Attributes set on VCRConnection now also get set on the real_connection 2015-09-03 14:35:28 -04:00
Ivan 'Goat' Malison
34252bc234 Merge pull request #192 from agriffis/insensitive-headers
Make request.headers always a CaseInsensitiveDict.
2015-08-28 14:47:45 -07:00
Ivan Malison
5f78657c52 test_boto cleanup 2015-08-28 13:03:09 -07:00
Aron Griffis
00b4e451fe Full image URL in README.rst
...so it shows properly on PyPI
2015-08-28 14:46:08 -04:00
Aron Griffis
44564ba39f Merge pull request #198 from gwillem/patch-1
Add instructions on using libyaml
2015-08-28 13:01:14 -04:00
Aron Griffis
7f02a7e999 Fix a bad merge in README.rst
This change seems to have gotten lost in #200
2015-08-28 12:57:05 -04:00
Ivan Malison
c28adea66d Fix typo in release notes 2015-08-28 09:20:54 -07:00
Aron Griffis
3f006cc261 Merge pull request #201 from agriffis/path-transformer-default
Default path_transformer=None. Fixes #199
2015-08-28 07:55:13 -04:00
Aron Griffis
0eda8ba482 Default path_transformer=None. Fixes #199 2015-08-28 07:25:03 -04:00
Ivan 'Goat' Malison
d620095c36 Merge pull request #200 from kevin1024/test_class_meta
Test class meta
2015-08-28 03:19:54 -07:00
Ivan Malison
c8180326ad Automatically decorate dynamically added methods with auto_decorate 2015-08-28 02:17:01 -07:00
Willem de Groot
d55d593d1c Changed bash to sh and fix syntax
Online Github editting is not very convenient after all :)
2015-08-28 11:14:01 +02:00
Willem de Groot
04f4a7fd2f Add bash code 2015-08-28 11:11:03 +02:00
Ivan Malison
6fd04f3675 Add test_case method to VCR
this method provides a class that can be inherited from to decorate all
test methods on the desired class with use_cassette using the relevant vcr
2015-08-28 01:44:39 -07:00
Ivan Malison
420f83b6b1 Move badges to top of README. 2015-08-28 01:44:39 -07:00
Aron Griffis
c6adcc83b3 Spello on use_cassette, thanks @Diaoul 2015-08-27 13:39:01 -04:00
Willem de Groot
dc61f5f520 Typoe 2015-08-27 11:20:11 +02:00
Willem de Groot
4450cb992f Add instructions on using libyaml
For a 10x speed increase!
2015-08-27 11:19:31 +02:00
Aron Griffis
083b1ec686 Move gitter chat with other badges 2015-08-26 17:34:45 -04:00
Ivan 'Goat' Malison
97c924d8dd Merge pull request #197 from gitter-badger/gitter-badge
Add a Gitter chat badge to README.rst
2015-08-26 11:49:26 -07:00
The Gitter Badger
65398131a4 Added Gitter badge 2015-08-26 17:29:20 +00:00
Aron Griffis
7312229aef Add HeadersDict, and mark add_header deprecated.
HeadersDict is a subclass of CaseInsensitiveDict with two new features:

  1. Preserve the case of the header key from the first time it was set.
     This means that later munging won't modify the key case. (You can
     force picking up the new case with `del` followed by setting.)

  2. If the value is a list or tuple, unpack it and store the first
     element. This is the same as how `Request.add_header()` used to work.

For backward compatibility this commit preserves `Request.add_header()` but
marks it deprecated.
2015-08-25 06:30:50 -04:00
Ivan 'Goat' Malison
b62265c0ad Merge pull request #194 from agriffis/travis-requests26
Add requests-2.6.0 to travis
2015-08-24 15:14:12 -07:00
Ivan 'Goat' Malison
d00c60a4ad Merge pull request #193 from agriffis/new-travis
Container-based travis for faster tests
2015-08-24 15:13:59 -07:00
Aron Griffis
4ddfb47c9c Add requests-2.6.0 to travis
This is tested already in tox, but seems to have been omitted accidentally
from the travis config.
2015-08-24 16:48:54 -04:00
Aron Griffis
f0b7c3f1e0 Container-based travis for faster tests 2015-08-24 16:28:29 -04:00
Aron Griffis
646d12df94 More compact expression with dict.get() 2015-08-24 16:19:34 -04:00
Aron Griffis
eda64bc3be Make request.headers always a CaseInsensitiveDict.
Previously request.headers was a normal dict (albeit with the
request.add_header interface) which meant that some code paths would do
case-sensitive matching, for example remove_post_data_parameters which
tests for 'Content-Type'. This change allows all code paths to get the same
case-insensitive treatment.

Additionally request.headers becomes a property to enforce upgrading it to
a CaseInsensitiveDict even if assigned.
2015-08-24 16:19:34 -04:00
Ivan Malison
efe6744eda v1.7.3 2015-08-23 18:10:01 -07:00
Ivan 'Goat' Malison
58f4b98f7f Merge pull request #191 from agriffis/trivial-fixes
Trivial cleanups and one bugfix
2015-08-23 13:33:34 -07:00
Aron Griffis
3305f0ca7d Repair a docstring 2015-08-23 16:33:25 -04:00
Aron Griffis
7f02d65dd9 Make hosts_to_ignore a set() earlier for clarity. 2015-08-23 16:00:52 -04:00
Aron Griffis
3e5553c56a Don't drop passed-in before_record_response
This is an actual bugfix. If before_record_response is passed into VCR as
an iterable then it won't be included in filter_functions. This commit
repairs the logic to separate the tests (just as it's already done for
before_record_request).

Also use .extend() rather than looping on .append()
2015-08-23 16:00:52 -04:00
Aron Griffis
a569dd4dc8 Raise KeyError with message instead of print, just like in get_matchers below 2015-08-23 12:37:01 -04:00
Aron Griffis
eb1cdad03a self._before_record_response can never be falsy in Cassette (just like self._before_record_request above it) 2015-08-23 12:37:01 -04:00
Aron Griffis
08bb3bd187 Remove an extra space 2015-08-23 12:37:01 -04:00
Ivan Malison
ae5580c8f9 style changes in test_vcr.py 2015-08-22 18:59:59 -07:00
Ivan Malison
f342f92f03 additional_matchers test 2015-08-22 18:58:12 -07:00
Ivan Malison
be3bf39161 Style changes in vcr/config.py 2015-08-22 18:48:04 -07:00
Ivan Malison
29d37e410a Add additional_matchers to use_cassette
Closes #188.
2015-08-22 18:06:13 -07:00
Ivan Malison
8b7e6c0ab8 v1.7.2 2015-08-18 17:00:45 -07:00
Ivan Malison
bd7c6ed03f Update comment about reentrance on cassette.py 2015-08-18 16:17:41 -07:00
Ivan 'Goat' Malison
1e414826e7 Merge pull request #187 from abhinav/master
Set request_time on Tornadoo HTTPResponses
2015-08-18 16:10:42 -07:00
Abhinav Gupta
1e1c093b3c Set request_time on Tornadoo HTTPResponses 2015-08-18 15:53:35 -07:00
Ivan 'Goat' Malison
bb8f563135 Merge pull request #186 from ByteInternet/capture-effective-url
Capture effective url in Tornado
2015-08-18 01:55:24 -07:00
Maarten van Schaik
ca3200d96e Add test for urllib2 2015-08-14 12:42:17 +02:00
Maarten van Schaik
04b5978adc Add effective url test for httplib2 2015-08-14 12:37:34 +02:00
Maarten van Schaik
01f1f9fdc1 Verify effective_url is ok 2015-08-14 12:29:50 +02:00
Maarten van Schaik
a82e8628c2 Requests actually stores redirected request 2015-08-14 12:28:41 +02:00
Maarten van Schaik
7d68f0577a Capture effective URL in tornado 2015-08-14 12:08:57 +02:00
Ivan Malison
d0aa5fddb7 1.7.1 2015-08-12 12:21:29 -07:00
Kevin McCarthy
e54aeadc68 Merge pull request #184 from abhinav/master
For Tornado AsyncHTTPClient, replace the methods instead of the class.
2015-08-12 09:16:22 -10:00
Abhinav Gupta
c4a33d1cff For Tornado AsyncHTTPClient, replace the methods instead of the class.
This makes it so patching works even if the user has a reference to, or an
instance of the original unpatched AsyncHTTPClient class.

Fixes #183.
2015-08-12 10:51:08 -07:00
Ivan Malison
8b59d73f25 v1.7.0 2015-08-02 13:36:57 -07:00
Ivan 'Goat' Malison
eb394b90d9 Merge pull request #181 from coagulant/fix-readme-custom-response
Fix example for custom response filtering in docs
2015-08-01 06:06:34 -07:00
Ilya Baryshev
14931dd47a Fix example for custom response filtering in docs 2015-08-01 11:10:51 +03:00
Ivan Malison
89cdda86d1 fix generator test. 2015-07-30 14:22:16 -07:00
Ivan 'Goat' Malison
ad48d71897 Merge pull request #180 from abhinav/master
Fix exception catching in coroutines.
2015-07-30 14:21:44 -07:00
Abhinav Gupta
946ce17a97 Fix exception catching in coroutines. 2015-07-30 14:13:58 -07:00
Ivan Malison
4d438dac75 Fix tornado python3 tests. 2015-07-30 04:19:17 -07:00
Ivan Malison
a234ad6b12 Fix all the tests in python 3 2015-07-30 03:39:04 -07:00
Ivan Malison
1d000ac652 Fix all the failing tests 2015-07-30 02:08:42 -07:00
Ivan Malison
21c176ee1e Make cassette active for duration of coroutine/generator
Closes #177.
2015-07-30 01:47:29 -07:00
Ivan 'Goat' Malison
4fb5bef8e1 Merge pull request #179 from graingert/support-ancient-PyPA-tools
Support distribute, Fixes #178
2015-07-29 23:22:16 -07:00
Thomas Grainger
9717596e2c Support distribute, Fixes #178 2015-07-29 22:19:45 +01:00
Ivan 'Goat' Malison
1660cc3a9f Merge pull request #176 from charlax/patch-1
Make setup example pep8 compliant
2015-07-27 10:37:42 -07:00
Charles-Axel Dein
4beb023204 Make setup example pep8 compliant
Pretty minor doc change.
2015-07-27 18:22:51 +02:00
Ivan 'Goat' Malison
72eb5345d6 Merge pull request #175 from gward/issue163-v2
Fix for #163, take 2.
2015-07-26 02:40:27 -07:00
Greg Ward
fe7d193d1a Add several more test cases for issue #163. 2015-07-16 14:49:48 -04:00
Greg Ward
09b7ccf561 Ensure that request bodies are always bytes, not text (fixes #163).
It shouldn't matter whether the request body comes from a file or a
string, or whether it is passed to the Request constructor or assigned
later. It should always be stored internally as bytes.
2015-07-16 14:36:26 -04:00
Kevin McCarthy
a4a80b431b Merge pull request #173 from graingert/patch-2
Fix before_record_reponse doc
2015-07-16 07:28:25 -10:00
Thomas Grainger
025a3b422d Fix before_record_reponse doc 2015-07-16 15:13:19 +01:00
Kevin McCarthy
bb05b2fcf7 Merge pull request #172 from abhinav/patch-1
Add Tornado to list of supported libraries
2015-07-15 10:31:51 -10:00
Abhinav Gupta
f77ef81877 Add Tornado to list of supported libraries 2015-07-15 12:43:36 -07:00
Ivan Malison
80ece7750f v1.6.1 2015-07-15 00:25:56 -07:00
Ivan Malison
8a86d75dc5 Merge remote-tracking branch 'upstream/master' into improved_body_matcher 2015-07-15 00:10:37 -07:00
Ivan Malison
33a4fb98c6 Update unit tests for body matcher. Simplified logic. 2015-07-15 00:01:31 -07:00
Diaoul
a046697567 Add a read_body helper function 2015-07-15 01:16:10 +02:00
Diaoul
c0286dfd97 Add body matcher unit tests 2015-07-11 23:22:42 +02:00
Diaoul
cc9af1d5fb Use CaseInsensitiveDict in body matcher 2015-07-11 23:18:45 +02:00
Ivan 'Goat' Malison
5f8407a8a1 Merge pull request #170 from graingert/manual-conditional-requirements-for-old-pip
Support conditional requirements in old versions of pip
2015-07-07 16:09:46 -07:00
Thomas Grainger
c789c82c1d Support conditional requirements in old versions of pip 2015-07-07 11:28:49 +01:00
Ivan 'Goat' Malison
16b5b77bcd Merge pull request #168 from graingert/patch-1
Fix RST parse errors generated by pandoc
2015-07-05 12:43:43 -07:00
Thomas Grainger
0a093786ed Fix RST parse errors generated by pandoc 2015-07-05 12:14:01 +01:00
Diaoul
3986caf182 Use Content-Type based approach for body matcher
When converting objects to body, dicts and sets order can change
resulting in a different but same body. This fixes the issue by
comparing the enclosed data in the body rather than the body itself
while still allowing raw body matching with the raw_body matcher.
2015-07-04 19:21:14 +02:00
Ivan 'Goat' Malison
cc6c26646c Merge pull request #165 from abhinav/master
[Tornado] Fix unsupported features exception not being raised.
2015-07-03 16:04:49 -07:00
Abhinav Gupta
3846a4ccef [Tornado] Fix unsupported features exception not being raised.
Add tests for that exception being raisd correctly and for
CannotOverwriteCassetteException.
2015-07-03 12:34:57 -07:00
Ivan Malison
aae4ae255b README spacing fix. 2015-07-03 10:26:52 -07:00
Ivan Malison
92303a911a v1.6.0 2015-07-03 10:17:44 -07:00
Ivan 'Goat' Malison
57e0e6c753 Merge pull request #164 from abhinav/tornado
Tornado support
2015-07-03 10:01:20 -07:00
Abhinav Gupta
c37d607b97 Don't install pycurl if pypy is being used.
Pycurl doesn't yet support pypy.
2015-07-02 17:14:00 -07:00
Abhinav Gupta
7922fec9fe Tornado support 2015-07-02 14:33:34 -07:00
Ivan 'Goat' Malison
7d175b0f91 Merge pull request #162 from graingert/conditional-requirements
use conditional requirements for backport libraries closes #147
2015-07-02 09:25:10 -07:00
Thomas Grainger
41949f7dc6 use conditional requirements for backport libraries Fixes #147 2015-07-02 09:34:27 +01:00
Ivan 'Goat' Malison
d14888ccd8 Merge pull request #161 from graingert/packaging-fixes
packaging fixes
2015-06-30 12:02:38 -07:00
Thomas Grainger
a9ede54064 packaging fixes 2015-06-30 12:19:05 +01:00
Ivan 'Goat' Malison
ce7ceb0a1e Merge pull request #159 from MrJohz/master
Fix issue #158
2015-06-25 01:04:12 -04:00
Jonathan
e742d32a8a Update test to ensure that filter is correctly applied 2015-06-24 16:27:45 +01:00
Jonathan
ccc1ccaa0e Allow filtering post params in requests 2015-06-24 16:23:00 +01:00
Ivan Malison
731a33a79a test for xmlrpclib to make sure #140 is actually fixed. 2015-06-09 01:13:58 -07:00
Antoine Bertin
6cbc0fb279 Fix httplib endheaders if a message_body exists
Fixes #140
2015-06-09 00:59:43 -07:00
Ivan Malison
789f118c98 whitespace change. 2015-06-09 00:54:54 -07:00
Ivan Malison
4f07cb5257 Fix tox.ini requests26: requests==2.6.0 2015-06-09 00:53:28 -07:00
Ivan Malison
ad6f635ac2 Version 1.5.2. 2015-05-15 16:54:40 -07:00
Ivan 'Goat' Malison
142b840eee Merge pull request #155 from gazpachoking/explicit_paths
Crash when cassette path contains cassette_library_dir
2015-05-15 16:51:03 -07:00
Chase Sterling
32c687522d Fix bug when specifying cassette path containing cassette_library_dir 2015-05-15 19:07:18 -04:00
Chase Sterling
5fc33c7e70 Add tests for explicitly specifying a path when library dir is defined 2015-05-15 19:06:05 -04:00
Ivan Malison
0f81f023c8 Fix readme, version 1.5.1 2015-05-14 14:46:14 -07:00
Ivan Malison
e324a9677d version 1.5.0 2015-05-14 14:05:50 -07:00
Ivan Malison
28640beb7d README updates. 2015-05-14 14:03:49 -07:00
Ivan 'Goat' Malison
c338d5d32c Merge pull request #154 from marco-santamaria/master
Filter parameters from 'application/json' content-type POST requests
2015-05-14 14:03:14 -07:00
marco.santamaria
59aa351ca8 Added support for json post data in filter_post_data_parameters. 2015-05-14 14:13:14 +02:00
Ivan Malison
2323b9da5f Automatically generate cassette names from function names. Add
`path_transformer` and `func_path_generator`. Closes #151.
2015-05-10 03:22:43 -07:00
Ivan Malison
0bbbc694b0 Make CassetteContextDecorator decorator produce reentrant functions.
Closes #150.
2015-05-09 23:14:00 -07:00
Kevin McCarthy
d293020617 Merge pull request #153 from addgene/mw/specify-six-version
Fix version of `six` dependency.
2015-05-07 20:34:38 -10:00
Morgan Wahl
daac863f0b Fixed version of six dependency.
`from six.moves.http_client import HTTPConnection` fails before version 1.5.0 of six. (on Python 2.7, at least.)
2015-05-07 15:35:26 -04:00
Kevin McCarthy
5cfb005b48 bump version 2015-05-05 22:22:37 -10:00
Kevin McCarthy
4ade547779 python3 uses capital headers sometimes, let's do a case-agnostic header removal 2015-05-05 21:59:48 -10:00
Kevin McCarthy
dc8eedf555 we dont actually need lxml for tests 2015-05-05 21:59:31 -10:00
Kevin McCarthy
5b9b6cd8b5 dont try to load chunked responses from cassettes, we already unchunked everything 2015-05-05 21:32:32 -10:00
Kevin McCarthy
856c38479a add failing test for requests2.7/gzip issue 2015-05-05 20:49:12 -10:00
Kevin McCarthy
52496cd091 really add requests 2.7 to tox 2015-05-05 19:50:20 -10:00
Kevin McCarthy
bc26ce877a add requests 2.7 to tox and travis 2015-05-05 19:44:19 -10:00
Kevin McCarthy
8db0d245a5 bump version 2015-04-11 11:21:31 -10:00
Kevin McCarthy
47544b08fe Merge pull request #148 from ralphbean/master
Ship extra bits with the pypi tarball.
2015-04-11 11:18:57 -10:00
Ralph Bean
4e560fc8db Ship extra bits with the pypi tarball.
This change should make new tarballs uploaded to pypi include various
nice pieces:

- The README
- The LICENSE
- The tests

The text of the license actually specifies that the full text must be
distributed with all copies of the software.  So, you need it to be in
compliance with the MIT license.

The README is just nice to have, and the tests are particularly nice for
my use case.  I am packaging vcrpy for inclusion in the Fedora linux
distribution and:

- We like to use the tarball from pypi because it is the same source
  distribution that everyone else is using.
- We like to run the tests before we build the rpm in our build system
  to make sure nothing crazy is going on.

Of course, we can use the tarball for the source and then do a second
step to clone the source and get the tests.  But, this is more work than
we like if we can just get the tests added to the tarball.  Other
distributions (like Debian) like this too.
2015-04-11 14:53:48 -04:00
Ivan Malison
8bb3c6beee v1.4.0 2015-04-02 12:20:16 -07:00
Ivan Malison
df3ad5f35c remove compat.py in favor of backport_collections. 2015-04-02 10:32:34 -07:00
Ivan Malison
e8a6a7a49f add backport_collections, tweaks to setup.py. 2015-04-02 10:23:22 -07:00
Ivan Malison
881138cb8d inject_cassette fallout. 2015-04-01 17:46:35 -07:00
Ivan Malison
639dba6f7a Write test for #145 that checks behavior of with_current_defaults. 2015-04-01 17:30:05 -07:00
Ivan Malison
b9bdc6401d inject_cassette kwarg. 2015-04-01 17:30:05 -07:00
Ivan Malison
3ca5529d26 Touch ups. 2015-04-01 15:38:59 -07:00
Sam Stavinoha
e3f2bc8369 fix with_current_defaults causing TypeError
The from_args() method in cassette.py was
throwing a TypeError when calling

    use_cassette(..., with_current_defaults=True)
    ...
    TypeError: from_args() takes exactly 3 arguments (4 given)

The path was then being passed to use() twice.
2015-03-31 21:08:26 -05:00
Edward Stone
fc4e985ee9 fallback to compat OrderedDict if collections.OrderedDict unavailable 2015-03-31 13:12:13 -07:00
Edward Stone
9038bc9066 fix docs for post data filter 2015-03-31 13:12:13 -07:00
Edward Stone
0def349420 Add ability to filter post data parameters 2015-03-31 13:12:13 -07:00
Ivan Malison
0dd7b05990 Get rid of all the constructor parameters that were removed in 0871c3b87c 2015-03-31 13:03:11 -07:00
Ivan Malison
630088599f Update copyright. 2015-03-31 13:02:59 -07:00
Ivan Malison
870ab276c4 Possible fix for #140. 2015-03-25 13:01:55 -07:00
Ivan 'Goat' Malison
779f3b0474 Merge pull request #141 from IvanMalison/post_files_through_requests
Add support for posting files through requests. closes #121
2015-03-24 16:43:13 -07:00
Ivan Malison
b948ed4857 Fix python3 support for requests file uploads. 2015-03-24 15:41:14 -07:00
Ivan Malison
c43e618635 Add mention of urllib3 support in readme. 2015-03-24 14:24:47 -07:00
Ivan Malison
5bd40a447a Add setter to body on vcr's request. 2015-03-24 14:11:16 -07:00
Ivan Malison
4b4be7f661 Don't use 2.7+ style ',' separated with. 2015-03-24 14:11:16 -07:00
Ivan Malison
6602a449b1 Add support for posting files through requests. closes #121. Possibly #134. 2015-03-24 14:11:16 -07:00
Ivan Malison
7cd7264034 fix tox/urllib3 stuff. 2015-03-24 14:10:14 -07:00
Ivan Malison
e9c690b9e7 Version 1.3.0. 2015-03-23 18:10:26 -07:00
Ivan Malison
bba5df2fbb clarifying comment in patch.py. 2015-03-23 18:00:23 -07:00
Ivan Malison
39c3b15e02 unused imports. 2015-03-23 17:56:46 -07:00
Ivan Malison
c87e6d6f6a Clarifying comments in patch.py. 2015-03-23 17:55:49 -07:00
Ivan Malison
5ab77e22db Use suggested emacs style coding statements (see https://www.python.org/dev/peps/pep-0263/). 2015-03-23 17:55:49 -07:00
Ivan 'Goat' Malison
ec6f27bbad Merge pull request #138 from aisch/patch-and-test-urllib3
update urllib3 patch/stub to be same as used for requests and add tests
2015-03-23 17:47:42 -07:00
aisch
8930c97ff7 rm unused imports 2015-03-23 13:56:48 -07:00
aisch
e6b43a0374 rename urllib3 patch method and rm unused imports from tests 2015-03-23 13:43:30 -07:00
aisch
63ec95be06 update urllib3 patch/stub to be same as used for requests and add tests 2015-03-23 12:12:49 -07:00
Kevin McCarthy
84c45b2742 Merge pull request #136 from abhinav/https-port-fix
Fix default port for HTTPS
2015-02-24 09:12:49 -10:00
Abhinav Gupta
87a25e9ab0 Fix httplib2 integration test. 2015-02-24 00:10:08 -08:00
Abhinav Gupta
2473bdb77a Fix default port for HTTPS. 2015-02-23 23:37:04 -08:00
Ivan 'Goat' Malison
32831d4151 Merge pull request #135 from RomuloOliveira/patch-1
Fix missing quotes on Custom Response Filtering
2015-01-28 12:02:57 -08:00
Rômulo Oliveira
4991d6f1c8 Fix missing quotes on Custom Response Filtering
Missing quotes are bad
2015-01-28 11:34:47 -02:00
Ivan Malison
14ef1e87f7 Add custom_patches section to README.md 2015-01-08 14:02:41 -08:00
Ivan 'Goat' Malison
fb14739cc1 Merge pull request #133 from IvanMalison/custom_patches
Custom patches
2015-01-08 11:08:55 -08:00
Ivan Malison
a7c7e4e279 Bump version to 1.2.0 2015-01-08 10:56:39 -08:00
Ivan Malison
c0a22df7ed Add ability to add custom patches to vcr and cassettes. 2015-01-08 10:54:27 -08:00
Ivan Malison
83aed99058 Bump vesrsion to 1.1.4, add to release notes. 2014-12-26 05:26:24 -05:00
Ivan Malison
e1f65bcbdc Add force reset around calls to actual connection from stubs, to ensure
compatibility with version of httplib/urlib2 in python 2.7.9. Closes #130.
2014-12-26 05:10:20 -05:00
Kevin McCarthy
5301149bd8 Merge pull request #128 from gazpachoking/patch-1
Update changelog to note requests 2.5 support
2014-12-09 08:55:52 -10:00
Chase Sterling
0297fcdde7 Update changelog to note requests 2.5 support 2014-12-09 13:26:46 -05:00
Kevin McCarthy
9480954c33 update release notes 2014-12-08 17:10:35 -10:00
Kevin McCarthy
8432ad32f1 Merge pull request #127 from gazpachoking/1.1.3
Version bump to v1.1.3
2014-12-08 17:09:07 -10:00
Chase Sterling
fabef3d988 Version bump to v1.1.3 2014-12-08 21:43:01 -05:00
Ivan 'Goat' Malison
da45f46b2d Merge pull request #125 from gazpachoking/pool_is_none
Fix crash with requests 2.5 where connectionpool was None
2014-12-08 13:20:36 -08:00
Ivan 'Goat' Malison
562a0ebadc Merge pull request #126 from gazpachoking/116
Play back requests requests on windows. fix #116
2014-12-08 12:29:34 -08:00
Chase Sterling
ef8ba6d51b Add requests 2.5 to testing list in .travis.yml and tox.ini 2014-12-08 14:40:55 -05:00
Chase Sterling
f6aa6eac84 Play back requests requests on windows. fix #116 2014-12-08 14:28:48 -05:00
Chase Sterling
821e148752 Fix crash with requests 2.5 where connectionpool was None 2014-12-07 13:49:23 -05:00
Ivan Malison
7306205b8a Improve test_new_episodes_record_mode_two_times test. 2014-11-21 17:15:15 -08:00
Nithin Reddy
2a128893cc Adds a test to ensure that the cassette created with "new_episodes" has different expected behavior when opened with "once". 2014-11-21 09:47:28 -08:00
Nithin Reddy
5162d183e5 Fixes #123. When attempting to replay the same request twice using record_mode="new_episodes", vcr.py raises UnhandledHTTPRequestError. 2014-11-20 19:07:21 -08:00
Ivan Malison
9d52c3ed42 Remove warning message caused by lack of is_verified property on HTTPSConnection stub. 2014-11-13 16:32:38 -08:00
Ivan 'Goat' Malison
0e37759175 Merge pull request #118 from rtaboada/fix-response-stub-headers-field
Create headers field in VCRHTTPResponse. Fixes #117.
2014-11-03 04:07:12 -08:00
Ivan 'Goat' Malison
78c6258ba3 Merge pull request #119 from telaviv/make_boto_tests_pass_again
test_boto_stubs passes again.
2014-10-31 00:14:45 -07:00
Shawn Krisman
b047336690 test_boto_stubs passes again. 2014-10-30 16:08:17 -07:00
Rodrigo Taboada
c955a5ea88 String in request body should be bytes. 2014-10-24 18:30:32 -02:00
Rodrigo Taboada
5423d99f5a Tests for VCRHTTPResponse headers field. 2014-10-24 17:40:51 -02:00
Rodrigo Taboada
a71c15f398 Create headers field in VCRHTTPResponse. Fixes #117. 2014-10-24 16:37:12 -02:00
83 changed files with 5784 additions and 1794 deletions

5
.gitignore vendored
View File

@@ -1,5 +1,7 @@
*.pyc
.tox
.cache
.pytest_cache/
build/
dist/
*.egg/
@@ -7,4 +9,5 @@ dist/
*.egg-info/
pytestdebug.log
fixtures/
fixtures/
/docs/_build

View File

@@ -1,38 +1,70 @@
language: python
sudo: false
before_install: openssl version
env:
global:
- secure: AifoKzwhjV94cmcQZrdQmqRu/9rkZZvWpwBv1daeAQpLOKFPGsOm3D+x2cSw9+iCfkgDZDfqQVv1kCaFVxTll8v8jTq5SJdqEY0NmGWbj/UkNtShh609oRDsuzLxAEwtVKYjf/h8K2BRea+bl1tGkwZ2vtmYS6dxNlAijjWOfds=
- secure: LBSEg/gMj4u4Hrpo3zs6Y/1mTpd2RtcN49mZIFgTdbJ9IhpiNPqcEt647Lz94F9Eses2x2WbNuKqZKZZReY7QLbEzU1m0nN5jlaKrjcG5NR5clNABfFFyhgc0jBikyS4abAG8jc2efeaTrFuQwdoF4sE8YiVrkiVj2X5Xoi6sBk=
matrix:
- WITH_LIB="requests2.2"
- WITH_LIB="requests2.3"
- WITH_LIB="requests2.4"
- WITH_LIB="requests1.x"
- WITH_LIB="httplib2"
- WITH_LIB="boto"
- TOX_SUFFIX="flakes"
- TOX_SUFFIX="requests27"
- TOX_SUFFIX="httplib2"
- TOX_SUFFIX="boto3"
- TOX_SUFFIX="urllib3121"
- TOX_SUFFIX="tornado4"
- TOX_SUFFIX="aiohttp"
matrix:
include:
- env: TOX_SUFFIX="flakes"
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: 3.7
dist: xenial
sudo: true
allow_failures:
- env: WITH_LIB="boto"
- env: TOX_SUFFIX="boto3"
- env: TOX_SUFFIX="aiohttp"
python: "pypy3.5-5.9.0"
exclude:
- env: WITH_LIB="boto"
python: 3.3
- env: WITH_LIB="boto"
python: 3.4
- env: WITH_LIB="requests1.x"
# 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: pypy
python:
- 2.6
- 2.7
- 3.3
- 3.4
- 3.5
- 3.6
- pypy
- "pypy3.5-5.9.0"
install:
- pip install PyYAML pytest pytest-localserver --use-mirrors
- if [ $WITH_LIB = "requests1.x" ] ; then pip install requests==1.2.3; fi
- if [ $WITH_LIB = "requests2.2" ] ; then pip install requests==2.2.1; fi
- if [ $WITH_LIB = "requests2.3" ] ; then pip install requests==2.3.0; fi
- if [ $WITH_LIB = "requests2.4" ] ; then pip install requests==2.4.0; fi
- if [ $WITH_LIB = "httplib2" ] ; then pip install httplib2; fi
- if [ $WITH_LIB = "boto" ] ; then pip install boto; fi
script: python setup.py test
- pip install tox-travis
- if [[ $TOX_SUFFIX != 'flakes' ]]; then python setup.py install ; fi
script:
- tox -e "${TOX_SUFFIX}"

View File

@@ -1,4 +1,4 @@
Copyright (c) 2012-2014 Kevin McCarthy
Copyright (c) 2012-2015 Kevin McCarthy
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

6
MANIFEST.in Normal file
View File

@@ -0,0 +1,6 @@
include README.rst
include LICENSE.txt
include tox.ini
recursive-include tests *
recursive-exclude * __pycache__
recursive-exclude * *.py[co]

554
README.md
View File

@@ -1,554 +0,0 @@
# VCR.py
![vcr.py](https://raw.github.com/kevin1024/vcrpy/master/vcr.png)
This is a Python version of [Ruby's VCR library](https://github.com/vcr/vcr).
[![Build Status](https://secure.travis-ci.org/kevin1024/vcrpy.png?branch=master)](http://travis-ci.org/kevin1024/vcrpy)
[![Stories in Ready](https://badge.waffle.io/kevin1024/vcrpy.png?label=ready&title=Ready)](https://waffle.io/kevin1024/vcrpy)
## What it does
Simplify and speed up testing HTTP by recording all HTTP interactions and
saving them to "cassette" files, which are yaml files containing the contents
of your requests and responses. Then when you run your tests again, they all
just hit the text files instead of the internet. This speeds up your tests and
lets you work offline.
If the server you are testing against ever changes its API, all you need to do
is delete your existing cassette files, and run your tests again. All of the
mocked responses will be updated with the new API.
## Compatibility Notes
VCR.py supports Python 2.6 and 2.7, 3.3, 3.4, and [pypy](http://pypy.org).
The following http libraries are supported:
* urllib2
* http.client (python3)
* requests (both 1.x and 2.x versions)
* httplib2
* boto
## Usage
```python
import vcr
import urllib2
with vcr.use_cassette('fixtures/vcr_cassettes/synopsis.yaml'):
response = urllib2.urlopen('http://www.iana.org/domains/reserved').read()
assert 'Example domains' in response
```
Run this test once, and VCR.py will record the HTTP request to
`fixtures/vcr_cassettes/synopsis.yml`. Run it again, and VCR.py will replay the
response from iana.org when the http request is made. This test is now fast (no
real HTTP requests are made anymore), deterministic (the test will continue to
pass, even if you are offline, or iana.org goes down for maintenance) and
accurate (the response will contain the same headers and body you get from a
real request).
You can also use VCR.py as a decorator. The same request above would look like
this:
```python
@vcr.use_cassette('fixtures/vcr_cassettes/synopsis.yaml')
def test_iana():
response = urllib2.urlopen('http://www.iana.org/domains/reserved').read()
assert 'Example domains' in response
```
All of the parameters and configuration works the same for the decorator
version.
## Configuration
If you don't like VCR's defaults, you can set options by instantiating a `VCR`
class and setting the options on it.
```python
import vcr
my_vcr = vcr.VCR(
serializer = 'json',
cassette_library_dir = 'fixtures/cassettes',
record_mode = 'once',
match_on = ['uri', 'method'],
)
with my_vcr.use_cassette('test.json'):
# your http code here
```
Otherwise, you can override options each time you use a cassette.
```python
with vcr.use_cassette('test.yml', serializer='json', record_mode='once'):
# your http code here
```
Note: Per-cassette overrides take precedence over the global config.
## Request matching
Request matching is configurable and allows you to change which requests VCR
considers identical. The default behavior is
`['method', 'scheme', 'host', 'port', 'path', 'query']` which means that
requests with both the same URL and method (ie POST or GET) are considered
identical.
This can be configured by changing the `match_on` setting.
The following options are available :
* method (for example, POST or GET)
* uri (the full URI.)
* host (the hostname of the server receiving the request)
* port (the port of the server receiving the request)
* path (the path of the request)
* query (the query string of the request)
* body (the entire request body)
* headers (the headers of the request)
Backwards compatible matchers:
* url (the `uri` alias)
If these options don't work for you, you can also register your own request
matcher. This is described in the Advanced section of this README.
## Record Modes
VCR supports 4 record modes (with the same behavior as Ruby's VCR):
### once
* Replay previously recorded interactions.
* Record new interactions if there is no cassette file.
* Cause an error to be raised for new requests if there is a cassette 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 changed).
once is the default record mode, used when you do not set one.
### new_episodes
* Record new interactions.
* Replay previously recorded interactions. It is similar to the once record
mode, but will always record new interactions, even if you have an existing
recorded one that is similar, but not identical.
This was the default behavior in versions < 0.3.0
### none
* Replay previously recorded interactions.
* Cause an error to be raised for any new requests. This is useful when your
code makes potentially dangerous HTTP requests. The none record mode
guarantees that no new HTTP requests will be made.
### all
* Record new interactions.
* Never replay previously recorded interactions. This can be temporarily used
to force VCR to re-record a cassette (i.e. to ensure the responses are not
out of date) or can be used when you simply want to log all HTTP requests.
## Advanced Features
If you want, VCR.py can return information about the cassette it is using to
record your requests and responses. This will let you record your requests and
responses and make assertions on them, to make sure that your code under test
is generating the expected requests and responses. This feature is not present
in Ruby's VCR, but I think it is a nice addition. Here's an example:
```python
import vcr
import urllib2
with vcr.use_cassette('fixtures/vcr_cassettes/synopsis.yaml') as cass:
response = urllib2.urlopen('http://www.zombo.com/').read()
# cass should have 1 request inside it
assert len(cass) == 1
# the request uri should have been http://www.zombo.com/
assert cass.requests[0].uri == 'http://www.zombo.com/'
```
The `Cassette` object exposes the following properties which I consider part of
the API. The fields are as follows:
* `requests`: A list of vcr.Request objects corresponding to the http requests
that were made during the recording of the cassette. The requests appear in the
order that they were originally processed.
* `responses`: A list of the responses made.
* `play_count`: The number of times this cassette has played back a response.
* `all_played`: A boolean indicating whether all the responses have been
played back.
* `responses_of(request)`: Access the responses that match a given request
The `Request` object has the following properties:
* `uri`: The full uri of the request. Example: "https://google.com/?q=vcrpy"
* `scheme`: The scheme used to make the request (http or https)
* `host`: The host of the request, for example "www.google.com"
* `port`: The port the request was made on
* `path`: The path of the request. For example "/" or "/home.html"
* `query`: The parsed query string of the request. Sorted list of name, value pairs.
* `method` : The method used to make the request, for example "GET" or "POST"
* `body`: The body of the request, usually empty except for POST / PUT / etc
Backwards compatible properties:
* `url`: The `uri` alias
* `protocol`: The `scheme` alias
## Register your own serializer
Don't like JSON or YAML? That's OK, VCR.py can serialize to any format you
would like. Create your own module or class instance with 2 methods:
* `def deserialize(cassette_string)`
* `def serialize(cassette_dict)`
Finally, register your class with VCR to use your new serializer.
```python
import vcr
class BogoSerializer(object):
"""
Must implement serialize() and deserialize() methods
"""
pass
my_vcr = vcr.VCR()
my_vcr.register_serializer('bogo', BogoSerializer())
with my_vcr.use_cassette('test.bogo', serializer='bogo'):
# your http here
# After you register, you can set the default serializer to your new serializer
my_vcr.serializer = 'bogo'
with my_vcr.use_cassette('test.bogo'):
# your http here
```
## Register your own request matcher
Create your own method with the following signature
```python
def my_matcher(r1, r2):
```
Your method receives the two requests and must return `True` if they match,
`False` if they don't.
Finally, register your method with VCR to use your new request matcher.
```python
import vcr
def jurassic_matcher(r1, r2):
return r1.uri == r2.uri and 'JURASSIC PARK' in r1.body
my_vcr = vcr.VCR()
my_vcr.register_matcher('jurassic', jurassic_matcher)
with my_vcr.use_cassette('test.yml', match_on=['jurassic']):
# your http here
# After you register, you can set the default match_on to use your new matcher
my_vcr.match_on = ['jurassic']
with my_vcr.use_cassette('test.yml'):
# your http here
```
## Filter sensitive data from the request
If you are checking your cassettes into source control, and are using some form
of authentication in your tests, you can filter out that information so it won't
appear in your cassette files. There are a few ways to do this:
### Filter information from HTTP Headers
Use the `filter_headers` configuration option with a list of headers to filter.
```python
with my_vcr.use_cassette('test.yml', filter_headers=['authorization']):
# sensitive HTTP request goes here
```
### Filter information from HTTP querystring
Use the `filter_query_parameters` configuration option with a list of query
parameters to filter.
```python
with my_vcr.use_cassette('test.yml', filter_query_parameters=['api_key']):
requests.get('http://api.com/getdata?api_key=secretstring')
```
### Custom Request filtering
If neither 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. Here is an example that will
never record requests to the /login endpoint.
```python
def before_record_cb(request):
if request.path != '/login':
return request
my_vcr = vcr.VCR(
before_record = before_record_cb,
)
with my_vcr.use_cassette('test.yml'):
# your http code here
```
You can also mutate the response using this callback. For example, you could
remove all query parameters from any requests to the `'/login'` path.
```python
def scrub_login_request(request):
if request.path == '/login':
request.uri, _ = urllib.splitquery(response.uri)
return request
my_vcr = vcr.VCR(
before_record=scrub_login_request,
)
with my_vcr.use_cassette('test.yml'):
# your http code here
```
### Custom Response Filtering
VCR.py also suports response filtering with the `before_record_response` keyword
argument. It's usage is similar to that of `before_record`:
```python
def scrub_string(string, replacement=''):
def before_record_reponse(response):
return response['body']['string] = response['body']['string].replace(string, replacement)
return scrub_string
my_vcr = vcr.VCR(
before_record=scrub_string(settings.USERNAME, 'username'),
)
with my_vcr.use_cassette('test.yml'):
# your http code here
```
## Ignore requests
If you would like to completely ignore certain requests, you can do it in a
few ways:
* Set the `ignore_localhost` option equal to True. This will not record any
requests sent to (or responses from) localhost, 127.0.0.1, or 0.0.0.0.
* Set the `ignore_hosts` configuration option to a list of hosts to ignore
* Add a `before_record` callback that returns None for requests you want to
ignore
Requests that are ignored by VCR will not be saved in a cassette, nor played
back from a cassette. VCR will completely ignore those requests as if it
didn't notice them at all, and they will continue to hit the server as if VCR
were not there.
## Installation
VCR.py is a package on PyPI, so you can `pip install vcrpy` (first you may need
to `brew install libyaml` [[Homebrew](http://mxcl.github.com/homebrew/)])
## Ruby VCR compatibility
VCR.py does not aim to match the format of the Ruby VCR YAML files. Cassettes
generated by Ruby's VCR are not compatible with VCR.py.
## Running VCR's test suite
The tests are all run automatically on [Travis
CI](https://travis-ci.org/kevin1024/vcrpy), but you can also run them yourself
using [py.test](http://pytest.org/) and [Tox](http://tox.testrun.org/). Tox
will automatically run them in all environments VCR.py supports. The test
suite is pretty big and slow, but you can tell tox to only run specific tests
like this:
`tox -e py27requests -- -v -k "'test_status_code or test_gzip'"`
This will run only tests that look like `test_status_code` or `test_gzip` in
the test suite, and only in the python 2.7 environment that has `requests`
installed.
Also, in order for the boto tests to run, you will need an AWS key. Refer to
the [boto
documentation](http://boto.readthedocs.org/en/latest/getting_started.html) 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 pull request.
## Logging
VCR.py has a few log messages you can turn on to help you figure out if HTTP
requests are hitting a real server or not. You can turn them on like this:
```python
import vcr
import requests
import logging
logging.basicConfig() # you need to initialize logging, otherwise you will not see anything from vcrpy
vcr_log = logging.getLogger("vcr")
vcr_log.setLevel(logging.INFO)
with vcr.use_cassette('headers.yml'):
requests.get('http://httpbin.org/headers')
```
The first time you run this, you will see:
```
INFO:vcr.stubs:<Request (GET) http://httpbin.org/headers> not in cassette, sending to real server
```
The second time, you will see:
```
INFO:vcr.stubs:Playing response for <Request (GET) http://httpbin.org/headers> from cassette
```
If you set the loglevel to DEBUG, you will also get information about which
matchers didn't match. This can help you with debugging custom matchers.
## Upgrade
### New Cassette Format
The cassette format has changed in _VCR.py 1.x_, the _VCR.py 0.x_ cassettes
cannot be used with _VCR.py 1.x_. The easiest way to upgrade is to simply
delete your cassettes and re-record all of them. VCR.py also provides a
migration script that attempts to upgrade your 0.x cassettes to the new 1.x
format. To use it, run the following command:
```
python -m vcr.migration PATH
```
The PATH can be either a path to the directory with cassettes or the path to a
single cassette.
*Note*: Back up your cassettes files before migration.
The migration *should* only modify cassettes using the old 0.x format.
## New serializer / deserializer API
If you made a custom serializer, you will need to update it to match the new
API in version 1.0.x
* Serializers now take dicts and return strings.
* Deserializers take strings and return dicts (instead of requests, responses
pair)
## Changelog
* 1.1.2 Add urllib==1.7.1 support. Make json serialize error handling correct
Improve logging of match failures.
* 1.1.1 Use function signature preserving `wrapt.decorator` to write the
decorator version of use_cassette in order to ensure compatibility with
py.test fixtures and python 2. Move all request filtering into the
`before_record_callable`.
* 1.1.0 Add `before_record_response`. Fix several bugs related to the context
management of cassettes.
* 1.0.3: Fix an issue with requests 2.4 and make sure case sensitivity is
consistent across python versions
* 1.0.2: Fix an issue with requests 2.3
* 1.0.1: Fix a bug with the new ignore requests feature and the once
record mode
* 1.0.0: _BACKWARDS INCOMPATIBLE_: Please see the 'upgrade' section in the
README. Take a look at the matcher section as well, you might want to
update your `match_on` settings. Add support for filtering sensitive
data from requests, matching query strings after the order changes and
improving the built-in matchers, (thanks to @mshytikov), support for
ignoring requests to certain hosts, bump supported Python3 version to
3.4, fix some bugs with Boto support (thanks @marusich), fix error with
URL field capitalization in README (thanks @simon-weber), added some log
messages to help with debugging, added `all_played` property on cassette
(thanks @mshytikov)
* 0.7.0: VCR.py now supports Python 3! (thanks @asundg) Also I refactored
the stub connections quite a bit to add support for the putrequest and
putheader calls. This version also adds support for httplib2 (thanks
@nilp0inter). I have added a couple tests for boto since it is an http
client in its own right. Finally, this version includes a fix for a bug
where requests wasn't being patched properly (thanks @msabramo).
* 0.6.0: Store response headers as a list since a HTTP response can have the
same header twice (happens with set-cookie sometimes). This has the added
benefit of preserving the order of headers. Thanks @smallcode for the bug
report leading to this change. I have made an effort to ensure backwards
compatibility with the old cassettes' header storage mechanism, but if you
want to upgrade to the new header storage, you should delete your
cassettes and re-record them. Also this release adds better error messages
(thanks @msabramo) and adds support for using VCR as a decorator (thanks
@smallcode for the motivation)
* 0.5.0: Change the `response_of` method to `responses_of` since cassettes
can now contain more than one response for a request. Since this changes
the API, I'm bumping the version. Also includes 2 bugfixes: a better error
message when attempting to overwrite a cassette file, and a fix for a bug
with requests sessions (thanks @msabramo)
* 0.4.0: Change default request recording behavior for multiple requests. If
you make the same request multiple times to the same URL, the response
might be different each time (maybe the response has a timestamp in it or
something), so this will make the same request multiple times and save them
all. Then, when you are replaying the cassette, the responses will be
played back in the same order in which they were received. If you were
making multiple requests to the same URL in a cassette before version
0.4.0, you might need to regenerate your cassette files. Also, removes
support for the cassette.play_count counter API, since individual requests
aren't unique anymore. A cassette might contain the same request several
times. Also removes secure overwrite feature since that was breaking
overwriting files in Windows, and fixes a bug preventing request's
automatic body decompression from working.
* 0.3.5: Fix compatibility with requests 2.x
* 0.3.4: Bugfix: close file before renaming it. This fixes an issue on
Windows. Thanks @smallcode for the fix.
* 0.3.3: Bugfix for error message when an unreigstered custom matcher was
used
* 0.3.2: Fix issue with new config syntax and the `match_on` parameter.
Thanks, @chromy!
* 0.3.1: Fix issue causing full paths to be sent on the HTTP request line.
* 0.3.0: *Backwards incompatible release* - Added support for record modes,
and changed the default recording behavior to the "once" record mode.
Please see the documentation on record modes for more. Added support for
custom request matching, and changed the default request matching behavior to
match only on the URL and method. Also, improved the httplib mocking to add
support for the `HTTPConnection.send()` method. This means that requests won't
actually be sent until the response is read, since I need to record the entire
request in order to match up the appropriate response. I don't think this
should cause any issues unless you are sending requests without ever loading
the response (which none of the standard httplib wrappers do, as far as I know.
Thanks to @fatuhoku for some of the ideas and the motivation behind this
release.
* 0.2.1: Fixed missing modules in setup.py
* 0.2.0: Added configuration API, which lets you configure some settings on
VCR (see the README). Also, VCR no longer saves cassettes if they haven't
changed at all and supports JSON as well as YAML (thanks @sirpengi). Added
amazing new skeumorphic logo, thanks @hairarrow.
* 0.1.0: *backwards incompatible release - delete your old cassette files*:
This release adds the ability to access the cassette to make assertions on
it, as well as a major code refactor thanks to @dlecocq. It also fixes a
couple longstanding bugs with redirects and HTTPS. [#3 and #4]
* 0.0.4: If you have libyaml installed, vcrpy will use the c bindings
instead. Speed up your tests! Thanks @dlecocq
* 0.0.3: Add support for requests 1.2.3. Support for older versions of
requests dropped (thanks @vitormazzi and @bryanhelmig)
* 0.0.2: Add support for requests / urllib3
* 0.0.1: Initial Release
# License
This library uses the MIT license. See [LICENSE.txt](LICENSE.txt) for more details

60
README.rst Normal file
View File

@@ -0,0 +1,60 @@
|PyPI| |Python versions| |Build Status| |Waffle Ready| |Gitter|
VCR.py
======
.. image:: https://raw.github.com/kevin1024/vcrpy/master/vcr.png
:alt: vcr.py
This is a Python version of `Ruby's VCR
library <https://github.com/vcr/vcr>`__.
Source code
https://github.com/kevin1024/vcrpy
Documentation
https://vcrpy.readthedocs.io/
Rationale
---------
VCR.py simplifies and speeds up tests that make HTTP requests. The
first time you run code that is inside a VCR.py context manager or
decorated function, VCR.py records all HTTP interactions that take
place through the libraries it supports and serializes and writes them
to a flat file (in yaml format by default). This flat file is called a
cassette. When the relevant piece of code is executed again, VCR.py
will read the serialized requests and responses from the
aforementioned cassette file, and intercept any HTTP requests that it
recognizes from the original test run and return the responses that
corresponded to those requests. This means that the requests will not
actually result in HTTP traffic, which confers several benefits
including:
- The ability to work offline
- Completely deterministic tests
- Increased test execution speed
If the server you are testing against ever changes its API, all you need
to do is delete your existing cassette files, and run your tests again.
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.
License
=======
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
.. |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.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
:target: https://gitter.im/kevin1024/vcrpy?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge

192
docs/Makefile Normal file
View File

@@ -0,0 +1,192 @@
# Makefile for Sphinx documentation
#
# You can set these variables from the command line.
SPHINXOPTS =
SPHINXBUILD = sphinx-build
PAPER =
BUILDDIR = _build
# User-friendly check for sphinx-build
ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1)
$(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/)
endif
# Internal variables.
PAPEROPT_a4 = -D latex_paper_size=a4
PAPEROPT_letter = -D latex_paper_size=letter
ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
# the i18n builder cannot share the environment and doctrees with the others
I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest coverage gettext
help:
@echo "Please use \`make <target>' where <target> is one of"
@echo " html to make standalone HTML files"
@echo " dirhtml to make HTML files named index.html in directories"
@echo " singlehtml to make a single large HTML file"
@echo " pickle to make pickle files"
@echo " json to make JSON files"
@echo " htmlhelp to make HTML files and a HTML help project"
@echo " qthelp to make HTML files and a qthelp project"
@echo " applehelp to make an Apple Help Book"
@echo " devhelp to make HTML files and a Devhelp project"
@echo " epub to make an epub"
@echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
@echo " latexpdf to make LaTeX files and run them through pdflatex"
@echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx"
@echo " text to make text files"
@echo " man to make manual pages"
@echo " texinfo to make Texinfo files"
@echo " info to make Texinfo files and run them through makeinfo"
@echo " gettext to make PO message catalogs"
@echo " changes to make an overview of all changed/added/deprecated items"
@echo " xml to make Docutils-native XML files"
@echo " pseudoxml to make pseudoxml-XML files for display purposes"
@echo " linkcheck to check all external links for integrity"
@echo " doctest to run all doctests embedded in the documentation (if enabled)"
@echo " coverage to run coverage check of the documentation (if enabled)"
clean:
rm -rf $(BUILDDIR)/*
html:
$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
dirhtml:
$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
singlehtml:
$(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
@echo
@echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
pickle:
$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
@echo
@echo "Build finished; now you can process the pickle files."
json:
$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
@echo
@echo "Build finished; now you can process the JSON files."
htmlhelp:
$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
@echo
@echo "Build finished; now you can run HTML Help Workshop with the" \
".hhp project file in $(BUILDDIR)/htmlhelp."
qthelp:
$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
@echo
@echo "Build finished; now you can run "qcollectiongenerator" with the" \
".qhcp project file in $(BUILDDIR)/qthelp, like this:"
@echo "# qcollectiongenerator $(BUILDDIR)/qthelp/vcrpy.qhcp"
@echo "To view the help file:"
@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/vcrpy.qhc"
applehelp:
$(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp
@echo
@echo "Build finished. The help book is in $(BUILDDIR)/applehelp."
@echo "N.B. You won't be able to view it unless you put it in" \
"~/Library/Documentation/Help or install it in your application" \
"bundle."
devhelp:
$(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
@echo
@echo "Build finished."
@echo "To view the help file:"
@echo "# mkdir -p $$HOME/.local/share/devhelp/vcrpy"
@echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/vcrpy"
@echo "# devhelp"
epub:
$(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
@echo
@echo "Build finished. The epub file is in $(BUILDDIR)/epub."
latex:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo
@echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
@echo "Run \`make' in that directory to run these through (pdf)latex" \
"(use \`make latexpdf' here to do that automatically)."
latexpdf:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo "Running LaTeX files through pdflatex..."
$(MAKE) -C $(BUILDDIR)/latex all-pdf
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
latexpdfja:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo "Running LaTeX files through platex and dvipdfmx..."
$(MAKE) -C $(BUILDDIR)/latex all-pdf-ja
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
text:
$(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
@echo
@echo "Build finished. The text files are in $(BUILDDIR)/text."
man:
$(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
@echo
@echo "Build finished. The manual pages are in $(BUILDDIR)/man."
texinfo:
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
@echo
@echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
@echo "Run \`make' in that directory to run these through makeinfo" \
"(use \`make info' here to do that automatically)."
info:
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
@echo "Running Texinfo files through makeinfo..."
make -C $(BUILDDIR)/texinfo info
@echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
gettext:
$(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
@echo
@echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
changes:
$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
@echo
@echo "The overview file is in $(BUILDDIR)/changes."
linkcheck:
$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
@echo
@echo "Link check complete; look for any errors in the above output " \
"or in $(BUILDDIR)/linkcheck/output.txt."
doctest:
$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
@echo "Testing of doctests in the sources finished, look at the " \
"results in $(BUILDDIR)/doctest/output.txt."
coverage:
$(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage
@echo "Testing of coverage in the sources finished, look at the " \
"results in $(BUILDDIR)/coverage/python.txt."
xml:
$(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml
@echo
@echo "Build finished. The XML files are in $(BUILDDIR)/xml."
pseudoxml:
$(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml
@echo
@echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml."

366
docs/advanced.rst Normal file
View File

@@ -0,0 +1,366 @@
Advanced Features
=================
If you want, VCR.py can return information about the cassette it is
using to record your requests and responses. This will let you record
your requests and responses and make assertions on them, to make sure
that your code under test is generating the expected requests and
responses. This feature is not present in Ruby's VCR, but I think it is
a nice addition. Here's an example:
.. code:: python
import vcr
import urllib2
with vcr.use_cassette('fixtures/vcr_cassettes/synopsis.yaml') as cass:
response = urllib2.urlopen('http://www.zombo.com/').read()
# cass should have 1 request inside it
assert len(cass) == 1
# the request uri should have been http://www.zombo.com/
assert cass.requests[0].uri == 'http://www.zombo.com/'
The ``Cassette`` object exposes the following properties which I
consider part of the API. The fields are as follows:
- ``requests``: A list of vcr.Request objects corresponding to the http
requests that were made during the recording of the cassette. The
requests appear in the order that they were originally processed.
- ``responses``: A list of the responses made.
- ``play_count``: The number of times this cassette has played back a
response.
- ``all_played``: A boolean indicating whether all the responses have
been played back.
- ``responses_of(request)``: Access the responses that match a given
request
The ``Request`` object has the following properties:
- ``uri``: The full uri of the request. Example:
"https://google.com/?q=vcrpy"
- ``scheme``: The scheme used to make the request (http or https)
- ``host``: The host of the request, for example "www.google.com"
- ``port``: The port the request was made on
- ``path``: The path of the request. For example "/" or "/home.html"
- ``query``: The parsed query string of the request. Sorted list of
name, value pairs.
- ``method`` : The method used to make the request, for example "GET"
or "POST"
- ``body``: The body of the request, usually empty except for POST /
PUT / etc
Backwards compatible properties:
- ``url``: The ``uri`` alias
- ``protocol``: The ``scheme`` alias
Register your own serializer
----------------------------
Don't like JSON or YAML? That's OK, VCR.py can serialize to any format
you would like. Create your own module or class instance with 2 methods:
- ``def deserialize(cassette_string)``
- ``def serialize(cassette_dict)``
Finally, register your class with VCR to use your new serializer.
.. code:: python
import vcr
class BogoSerializer(object):
"""
Must implement serialize() and deserialize() methods
"""
pass
my_vcr = vcr.VCR()
my_vcr.register_serializer('bogo', BogoSerializer())
with my_vcr.use_cassette('test.bogo', serializer='bogo'):
# your http here
# After you register, you can set the default serializer to your new serializer
my_vcr.serializer = 'bogo'
with my_vcr.use_cassette('test.bogo'):
# your http here
Register your own request matcher
---------------------------------
Create your own method with the following signature
.. code:: python
def my_matcher(r1, r2):
Your method receives the two requests and must return ``True`` if they
match, ``False`` if they don't.
Finally, register your method with VCR to use your new request matcher.
.. code:: python
import vcr
def jurassic_matcher(r1, r2):
return r1.uri == r2.uri and 'JURASSIC PARK' in r1.body
my_vcr = vcr.VCR()
my_vcr.register_matcher('jurassic', jurassic_matcher)
with my_vcr.use_cassette('test.yml', match_on=['jurassic']):
# your http here
# After you register, you can set the default match_on to use your new matcher
my_vcr.match_on = ['jurassic']
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
--------------------------------------
If you are checking your cassettes into source control, and are using
some form of authentication in your tests, you can filter out that
information so it won't appear in your cassette files. There are a few
ways to do this:
Filter information from HTTP Headers
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Use the ``filter_headers`` configuration option with a list of headers
to filter.
.. code:: python
with my_vcr.use_cassette('test.yml', filter_headers=['authorization']):
# sensitive HTTP request goes here
Filter information from HTTP querystring
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Use the ``filter_query_parameters`` configuration option with a list of
query parameters to filter.
.. code:: python
with my_vcr.use_cassette('test.yml', filter_query_parameters=['api_key']):
requests.get('http://api.com/getdata?api_key=secretstring')
Filter information from HTTP post data
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Use the ``filter_post_data_parameters`` configuration option with a list
of post data parameters to filter.
.. code:: python
with my_vcr.use_cassette('test.yml', filter_post_data_parameters=['client_secret']):
requests.post('http://api.com/postdata', data={'api_key': 'secretstring'})
Advanced use of filter_headers, filter_query_parameters and filter_post_data_parameters
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
In all of the above cases, it's also possible to pass a list of ``(key, value)``
tuples where the value can be any of the following:
* A new value to replace the original value.
* ``None`` to remove the key/value pair. (Same as passing a simple key string.)
* A callable that returns a new value or ``None``.
So these two calls are the same:
.. code:: python
# original (still works)
vcr = VCR(filter_headers=['authorization'])
# new
vcr = VCR(filter_headers=[('authorization', None)])
Here are two examples of the new functionality:
.. code:: python
# replace with a static value (most common)
vcr = VCR(filter_headers=[('authorization', 'XXXXXX')])
# replace with a callable, for example when testing
# lots of different kinds of authorization.
def replace_auth(key, value, request):
auth_type = value.split(' ', 1)[0]
return '{} {}'.format(auth_type, 'XXXXXX')
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_request`` configuration option to so this.
Here is an example that will never record requests to the /login
endpoint.
.. code:: python
def before_record_cb(request):
if request.path != '/login':
return request
my_vcr = vcr.VCR(
before_record_request = before_record_cb,
)
with my_vcr.use_cassette('test.yml'):
# your http code here
You can also mutate the response using this callback. For example, you
could remove all query parameters from any requests to the ``'/login'``
path.
.. code:: python
def scrub_login_request(request):
if request.path == '/login':
request.uri, _ = urllib.splitquery(response.uri)
return request
my_vcr = vcr.VCR(
before_record_request=scrub_login_request,
)
with my_vcr.use_cassette('test.yml'):
# your http code here
Custom Response Filtering
~~~~~~~~~~~~~~~~~~~~~~~~~
VCR.py also suports response filtering with the
``before_record_response`` keyword argument. It's usage is similar to
that of ``before_record``:
.. code:: python
def scrub_string(string, replacement=''):
def before_record_response(response):
response['body']['string'] = response['body']['string'].replace(string, replacement)
return response
return before_record_response
my_vcr = vcr.VCR(
before_record_response=scrub_string(settings.USERNAME, 'username'),
)
with my_vcr.use_cassette('test.yml'):
# your http code here
Decode compressed response
---------------------------
When the ``decode_compressed_response`` keyword argument of a ``VCR`` object
is set to True, VCR will decompress "gzip" and "deflate" response bodies
before recording. This ensures that these interactions become readable and
editable after being serialized.
.. note::
Decompression is done before any other specified `Custom Response Filtering`_.
This option should be avoided if the actual decompression of response bodies
is part of the functionality of the library or app being tested.
Ignore requests
---------------
If you would like to completely ignore certain requests, you can do it
in a few ways:
- Set the ``ignore_localhost`` option equal to True. This will not
record any requests sent to (or responses from) localhost, 127.0.0.1,
or 0.0.0.0.
- Set the ``ignore_hosts`` configuration option to a list of hosts to
ignore
- Add a ``before_record`` callback that returns None for requests you
want to ignore
Requests that are ignored by VCR will not be saved in a cassette, nor
played back from a cassette. VCR will completely ignore those requests
as if it didn't notice them at all, and they will continue to hit the
server as if VCR were not there.
Custom Patches
--------------
If you use a custom ``HTTPConnection`` class, or otherwise make http
requests in a way that requires additional patching, you can use the
``custom_patches`` keyword argument of the ``VCR`` and ``Cassette``
objects to patch those objects whenever a cassette's context is entered.
To patch a custom version of ``HTTPConnection`` you can do something
like this:
::
import where_the_custom_https_connection_lives
from vcr.stubs import VCRHTTPSConnection
my_vcr = config.VCR(custom_patches=((where_the_custom_https_connection_lives, 'CustomHTTPSConnection', VCRHTTPSConnection),))
@my_vcr.use_cassette(...)
Automatic Cassette Naming
-------------------------
VCR.py now allows the omission of the path argument to the use\_cassette
function. Both of the following are now legal/should work
.. code:: python
@my_vcr.use_cassette
def my_test_function():
...
.. code:: python
@my_vcr.use_cassette()
def my_test_function():
...
In both cases, VCR.py will use a path that is generated from the
provided test function's name. If no ``cassette_library_dir`` has been
set, the cassette will be in a file with the name of the test function
in directory of the file in which the test function is declared. If a
``cassette_library_dir`` has been set, the cassette will appear in that
directory in a file with the name of the decorated function.
It is possible to control the path produced by the automatic naming
machinery by customizing the ``path_transformer`` and
``func_path_generator`` vcr variables. To add an extension to all
cassette names, use ``VCR.ensure_suffix`` as follows:
.. code:: python
my_vcr = VCR(path_transformer=VCR.ensure_suffix('.yaml'))
@my_vcr.use_cassette
def my_test_function():

51
docs/api.rst Normal file
View File

@@ -0,0 +1,51 @@
API
===
:mod:`~vcr.config`
------------------
.. automodule:: vcr.config
:members:
:special-members: __init__
:mod:`~vcr.cassette`
--------------------
.. automodule:: vcr.cassette
:members:
:special-members: __init__
:mod:`~vcr.matchers`
--------------------
.. automodule:: vcr.matchers
:members:
:special-members: __init__
:mod:`~vcr.filters`
-------------------
.. automodule:: vcr.filters
:members:
:special-members: __init__
:mod:`~vcr.request`
-------------------
.. automodule:: vcr.request
:members:
:special-members: __init__
:mod:`~vcr.serialize`
---------------------
.. automodule:: vcr.serialize
:members:
:special-members: __init__
:mod:`~vcr.patch`
-----------------
.. automodule:: vcr.patch
:members:
:special-members: __init__

175
docs/changelog.rst Normal file
View File

@@ -0,0 +1,175 @@
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
@nickdirienzo)
- 1.8.0 Fix for Serialization errors with JSON adapter (thanks
@aliaksandrb). Avoid concatenating bytes with strings (thanks
@jaysonsantos). Exclude __pycache__ dirs & compiled files in sdist
(thanks @koobs). Fix Tornado support behavior for Tornado 3 (thanks
@abhinav). decode_compressed_response option and filter (thanks
@jayvdb).
- 1.7.4 [#217] Make use_cassette decorated functions actually return a
value (thanks @bcen). [#199] Fix path transfromation defaults.
Better headers dictionary management.
- 1.7.3 [#188] ``additional_matchers`` kwarg on ``use_cassette``.
[#191] Actually support passing multiple before_record_request
functions (thanks @agriffis).
- 1.7.2 [#186] Get effective_url in tornado (thanks @mvschaik), [#187]
Set request_time on Response object in tornado (thanks @abhinav).
- 1.7.1 [#183] Patch ``fetch_impl`` instead of the entire HTTPClient
class for Tornado (thanks @abhinav).
- 1.7.0 [#177] Properly support coroutine/generator decoration. [#178]
Support distribute (thanks @graingert). [#163] Make compatibility
between python2 and python3 recorded cassettes more robust (thanks
@gward).
- 1.6.1 [#169] Support conditional requirements in old versions of
pip, Fix RST parse errors generated by pandoc, [Tornado] Fix
unsupported features exception not being raised, [#166]
content-aware body matcher.
- 1.6.0 [#120] Tornado support (thanks @abhinav), [#147] packaging fixes
(thanks @graingert), [#158] allow filtering post params in requests
(thanks @MrJohz), [#140] add xmlrpclib support (thanks @Diaoul).
- 1.5.2 Fix crash when cassette path contains cassette library
directory (thanks @gazpachoking).
- 1.5.0 Automatic cassette naming and 'application/json' post data
filtering (thanks @marco-santamaria).
- 1.4.2 Fix a bug caused by requests 2.7 and chunked transfer encoding
- 1.4.1 Include README, tests, LICENSE in package. Thanks @ralphbean.
- 1.4.0 Filter post data parameters (thanks @eadmundo), support for
posting files through requests, inject\_cassette kwarg to access
cassette from ``use_cassette`` decorated function,
``with_current_defaults`` actually works (thanks @samstav).
- 1.3.0 Fix/add support for urllib3 (thanks @aisch), fix default port
for https (thanks @abhinav).
- 1.2.0 Add custom\_patches argument to VCR/Cassette objects to allow
users to stub custom classes when cassettes become active.
- 1.1.4 Add force reset around calls to actual connection from stubs,
to ensure compatibility with the version of httplib/urlib2 in python
2.7.9.
- 1.1.3 Fix python3 headers field (thanks @rtaboada), fix boto test
(thanks @telaviv), fix new\_episodes record mode (thanks @jashugan),
fix Windows connectionpool stub bug (thanks @gazpachoking), add
support for requests 2.5
- 1.1.2 Add urllib==1.7.1 support. Make json serialize error handling
correct Improve logging of match failures.
- 1.1.1 Use function signature preserving ``wrapt.decorator`` to write
the decorator version of use\_cassette in order to ensure
compatibility with py.test fixtures and python 2. Move all request
filtering into the ``before_record_callable``.
- 1.1.0 Add ``before_record_response``. Fix several bugs related to the
context management of cassettes.
- 1.0.3: Fix an issue with requests 2.4 and make sure case sensitivity
is consistent across python versions
- 1.0.2: Fix an issue with requests 2.3
- 1.0.1: Fix a bug with the new ignore requests feature and the once
record mode
- 1.0.0: *BACKWARDS INCOMPATIBLE*: Please see the 'upgrade' section in
the README. Take a look at the matcher section as well, you might
want to update your ``match_on`` settings. Add support for filtering
sensitive data from requests, matching query strings after the order
changes and improving the built-in matchers, (thanks to @mshytikov),
support for ignoring requests to certain hosts, bump supported
Python3 version to 3.4, fix some bugs with Boto support (thanks
@marusich), fix error with URL field capitalization in README (thanks
@simon-weber), added some log messages to help with debugging, added
``all_played`` property on cassette (thanks @mshytikov)
- 0.7.0: VCR.py now supports Python 3! (thanks @asundg) Also I
refactored the stub connections quite a bit to add support for the
putrequest and putheader calls. This version also adds support for
httplib2 (thanks @nilp0inter). I have added a couple tests for boto
since it is an http client in its own right. Finally, this version
includes a fix for a bug where requests wasn't being patched properly
(thanks @msabramo).
- 0.6.0: Store response headers as a list since a HTTP response can
have the same header twice (happens with set-cookie sometimes). This
has the added benefit of preserving the order of headers. Thanks
@smallcode for the bug report leading to this change. I have made an
effort to ensure backwards compatibility with the old cassettes'
header storage mechanism, but if you want to upgrade to the new
header storage, you should delete your cassettes and re-record them.
Also this release adds better error messages (thanks @msabramo) and
adds support for using VCR as a decorator (thanks @smallcode for the
motivation)
- 0.5.0: Change the ``response_of`` method to ``responses_of`` since
cassettes can now contain more than one response for a request. Since
this changes the API, I'm bumping the version. Also includes 2
bugfixes: a better error message when attempting to overwrite a
cassette file, and a fix for a bug with requests sessions (thanks
@msabramo)
- 0.4.0: Change default request recording behavior for multiple
requests. If you make the same request multiple times to the same
URL, the response might be different each time (maybe the response
has a timestamp in it or something), so this will make the same
request multiple times and save them all. Then, when you are
replaying the cassette, the responses will be played back in the same
order in which they were received. If you were making multiple
requests to the same URL in a cassette before version 0.4.0, you
might need to regenerate your cassette files. Also, removes support
for the cassette.play\_count counter API, since individual requests
aren't unique anymore. A cassette might contain the same request
several times. Also removes secure overwrite feature since that was
breaking overwriting files in Windows, and fixes a bug preventing
request's automatic body decompression from working.
- 0.3.5: Fix compatibility with requests 2.x
- 0.3.4: Bugfix: close file before renaming it. This fixes an issue on
Windows. Thanks @smallcode for the fix.
- 0.3.3: Bugfix for error message when an unreigstered custom matcher
was used
- 0.3.2: Fix issue with new config syntax and the ``match_on``
parameter. Thanks, @chromy!
- 0.3.1: Fix issue causing full paths to be sent on the HTTP request
line.
- 0.3.0: *Backwards incompatible release* - Added support for record
modes, and changed the default recording behavior to the "once"
record mode. Please see the documentation on record modes for more.
Added support for custom request matching, and changed the default
request matching behavior to match only on the URL and method. Also,
improved the httplib mocking to add support for the
``HTTPConnection.send()`` method. This means that requests won't
actually be sent until the response is read, since I need to record
the entire request in order to match up the appropriate response. I
don't think this should cause any issues unless you are sending
requests without ever loading the response (which none of the
standard httplib wrappers do, as far as I know. Thanks to @fatuhoku
for some of the ideas and the motivation behind this release.
- 0.2.1: Fixed missing modules in setup.py
- 0.2.0: Added configuration API, which lets you configure some
settings on VCR (see the README). Also, VCR no longer saves cassettes
if they haven't changed at all and supports JSON as well as YAML
(thanks @sirpengi). Added amazing new skeumorphic logo, thanks
@hairarrow.
- 0.1.0: *backwards incompatible release - delete your old cassette
files*: This release adds the ability to access the cassette to make
assertions on it, as well as a major code refactor thanks to
@dlecocq. It also fixes a couple longstanding bugs with redirects and
HTTPS. [#3 and #4]
- 0.0.4: If you have libyaml installed, vcrpy will use the c bindings
instead. Speed up your tests! Thanks @dlecocq
- 0.0.3: Add support for requests 1.2.3. Support for older versions of
requests dropped (thanks @vitormazzi and @bryanhelmig)
- 0.0.2: Add support for requests / urllib3
- 0.0.1: Initial Release

291
docs/conf.py Normal file
View File

@@ -0,0 +1,291 @@
# -*- coding: utf-8 -*-
#
# vcrpy documentation build configuration file, created by
# sphinx-quickstart on Sun Sep 13 11:18:00 2015.
#
# This file is execfile()d with the current directory set to its
# containing dir.
#
# Note that not all possible configuration values are present in this
# autogenerated file.
#
# All configuration values have a default; values that are commented out
# serve to show the default.
import os
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
#sys.path.insert(0, os.path.abspath('.'))
# -- General configuration ------------------------------------------------
# If your documentation needs a minimal Sphinx version, state it here.
#needs_sphinx = '1.0'
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = [
'sphinx.ext.autodoc',
'sphinx.ext.intersphinx',
'sphinx.ext.coverage',
'sphinx.ext.viewcode',
]
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
# The suffix(es) of source filenames.
# You can specify multiple suffix as a list of string:
# source_suffix = ['.rst', '.md']
source_suffix = '.rst'
# The encoding of source files.
#source_encoding = 'utf-8-sig'
# The master toctree document.
master_doc = 'index'
# General information about the project.
project = u'vcrpy'
copyright = u'2015, Kevin McCarthy'
author = u'Kevin McCarthy'
# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
# built documents.
#
# The short X.Y version.
version = '1.7.4'
# The full version, including alpha/beta/rc tags.
release = '1.7.4'
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
#
# This is also used if you do content translation via gettext catalogs.
# Usually you set "language" from the command line for these cases.
language = None
# There are two options for replacing |today|: either, you set today to some
# non-false value, then it is used:
#today = ''
# Else, today_fmt is used as the format for a strftime call.
#today_fmt = '%B %d, %Y'
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
exclude_patterns = ['_build']
# The reST default role (used for this markup: `text`) to use for all
# documents.
#default_role = None
# If true, '()' will be appended to :func: etc. cross-reference text.
#add_function_parentheses = True
# If true, the current module name will be prepended to all description
# unit titles (such as .. function::).
#add_module_names = True
# If true, sectionauthor and moduleauthor directives will be shown in the
# output. They are ignored by default.
#show_authors = False
# The name of the Pygments (syntax highlighting) style to use.
pygments_style = 'sphinx'
# A list of ignored prefixes for module index sorting.
#modindex_common_prefix = []
# If true, keep warnings as "system message" paragraphs in the built documents.
#keep_warnings = False
# If true, `todo` and `todoList` produce output, else they produce nothing.
todo_include_todos = False
# -- Options for HTML output ----------------------------------------------
# The theme to use for HTML and HTML Help pages.
# 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:
import sphinx_rtd_theme
html_theme = 'sphinx_rtd_theme'
html_theme_path = [sphinx_rtd_theme.get_html_theme_path()]
# Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the
# documentation.
#html_theme_options = {}
# The name for this set of Sphinx documents. If None, it defaults to
# "<project> v<release> documentation".
#html_title = None
# A shorter title for the navigation bar. Default is the same as html_title.
#html_short_title = None
# The name of an image file (relative to this directory) to place at the top
# of the sidebar.
#html_logo = None
# The name of an image file (within the static path) to use as favicon of the
# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
# pixels large.
#html_favicon = None
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['_static']
# Add any extra paths that contain custom files (such as robots.txt or
# .htaccess) here, relative to this directory. These files are copied
# directly to the root of the documentation.
#html_extra_path = []
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
# using the given strftime format.
#html_last_updated_fmt = '%b %d, %Y'
# If true, SmartyPants will be used to convert quotes and dashes to
# typographically correct entities.
#html_use_smartypants = True
# Custom sidebar templates, maps document names to template names.
#html_sidebars = {}
# Additional templates that should be rendered to pages, maps page names to
# template names.
#html_additional_pages = {}
# If false, no module index is generated.
#html_domain_indices = True
# If false, no index is generated.
#html_use_index = True
# If true, the index is split into individual pages for each letter.
#html_split_index = False
# If true, links to the reST sources are added to the pages.
#html_show_sourcelink = True
# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
#html_show_sphinx = True
# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
#html_show_copyright = True
# If true, an OpenSearch description file will be output, and all pages will
# contain a <link> tag referring to it. The value of this option must be the
# base URL from which the finished HTML is served.
#html_use_opensearch = ''
# This is the file name suffix for HTML files (e.g. ".xhtml").
#html_file_suffix = None
# Language to be used for generating the HTML full-text search index.
# Sphinx supports the following languages:
# 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja'
# 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr'
#html_search_language = 'en'
# A dictionary with options for the search language support, empty by default.
# Now only 'ja' uses this config value
#html_search_options = {'type': 'default'}
# The name of a javascript file (relative to the configuration directory) that
# implements a search results scorer. If empty, the default will be used.
#html_search_scorer = 'scorer.js'
# Output file base name for HTML help builder.
htmlhelp_basename = 'vcrpydoc'
# -- Options for LaTeX output ---------------------------------------------
latex_elements = {
# The paper size ('letterpaper' or 'a4paper').
#'papersize': 'letterpaper',
# The font size ('10pt', '11pt' or '12pt').
#'pointsize': '10pt',
# Additional stuff for the LaTeX preamble.
#'preamble': '',
# Latex figure (float) alignment
#'figure_align': 'htbp',
}
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title,
# author, documentclass [howto, manual, or own class]).
latex_documents = [
(master_doc, 'vcrpy.tex', u'vcrpy Documentation',
u'Kevin McCarthy', 'manual'),
]
# The name of an image file (relative to this directory) to place at the top of
# the title page.
#latex_logo = None
# For "manual" documents, if this is true, then toplevel headings are parts,
# not chapters.
#latex_use_parts = False
# If true, show page references after internal links.
#latex_show_pagerefs = False
# If true, show URL addresses after external links.
#latex_show_urls = False
# Documents to append as an appendix to all manuals.
#latex_appendices = []
# If false, no module index is generated.
#latex_domain_indices = True
# -- Options for manual page output ---------------------------------------
# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [
(master_doc, 'vcrpy', u'vcrpy Documentation',
[author], 1)
]
# If true, show URL addresses after external links.
#man_show_urls = False
# -- Options for Texinfo output -------------------------------------------
# Grouping the document tree into Texinfo files. List of tuples
# (source start file, target name, title, author,
# dir menu entry, description, category)
texinfo_documents = [
(master_doc, 'vcrpy', u'vcrpy Documentation',
author, 'vcrpy', 'One line description of project.',
'Miscellaneous'),
]
# Documents to append as an appendix to all manuals.
#texinfo_appendices = []
# If false, no module index is generated.
#texinfo_domain_indices = True
# How to display URL addresses: 'footnote', 'no', or 'inline'.
#texinfo_show_urls = 'footnote'
# If true, do not generate a @detailmenu in the "Top" node's menu.
#texinfo_no_detailmenu = False
# Example configuration for intersphinx: refer to the Python standard library.
intersphinx_mapping = {'https://docs.python.org/': None}

60
docs/configuration.rst Normal file
View File

@@ -0,0 +1,60 @@
Configuration
=============
If you don't like VCR's defaults, you can set options by instantiating a
``VCR`` class and setting the options on it.
.. code:: python
import vcr
my_vcr = vcr.VCR(
serializer='json',
cassette_library_dir='fixtures/cassettes',
record_mode='once',
match_on=['uri', 'method'],
)
with my_vcr.use_cassette('test.json'):
# your http code here
Otherwise, you can override options each time you use a cassette.
.. code:: python
with vcr.use_cassette('test.yml', serializer='json', record_mode='once'):
# your http code here
Note: Per-cassette overrides take precedence over the global config.
Request matching
----------------
Request matching is configurable and allows you to change which requests
VCR considers identical. The default behavior is
``['method', 'scheme', 'host', 'port', 'path', 'query']`` which means
that requests with both the same URL and method (ie POST or GET) are
considered identical.
This can be configured by changing the ``match_on`` setting.
The following options are available :
- method (for example, POST or GET)
- uri (the full URI.)
- host (the hostname of the server receiving the request)
- port (the port of the server receiving the request)
- path (the path of the request)
- query (the query string of the request)
- raw\_body (the entire request body as is)
- body (the entire request body unmarshalled by content-type
i.e. xmlrpc, json, form-urlencoded, falling back on raw\_body)
- headers (the headers of the request)
Backwards compatible matchers:
- url (the ``uri`` alias)
If these options don't work for you, you can also register your own
request matcher. This is described in the Advanced section of this
README.

25
docs/contributing.rst Normal file
View File

@@ -0,0 +1,25 @@
Contributing
============
Running VCR's test suite
------------------------
The tests are all run automatically on `Travis
CI <https://travis-ci.org/kevin1024/vcrpy>`__, but you can also run them
yourself using `py.test <http://pytest.org/>`__ and
`Tox <http://tox.testrun.org/>`__. Tox will automatically run them in
all environments VCR.py supports. The test suite is pretty big and slow,
but you can tell tox to only run specific tests like this::
tox -e py27requests -- -v -k "'test_status_code or test_gzip'"
This will run only tests that look like ``test_status_code`` or
``test_gzip`` in the test suite, and only in the python 2.7 environment
that has ``requests`` installed.
Also, in order for the boto tests to run, you will need an AWS key.
Refer to the `boto
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
Travis so you don't have to worry about them failing if you submit a
pull request.

31
docs/debugging.rst Normal file
View File

@@ -0,0 +1,31 @@
Debugging
=========
VCR.py has a few log messages you can turn on to help you figure out if
HTTP requests are hitting a real server or not. You can turn them on
like this:
.. code:: python
import vcr
import requests
import logging
logging.basicConfig() # you need to initialize logging, otherwise you will not see anything from vcrpy
vcr_log = logging.getLogger("vcr")
vcr_log.setLevel(logging.INFO)
with vcr.use_cassette('headers.yml'):
requests.get('http://httpbin.org/headers')
The first time you run this, you will see::
INFO:vcr.stubs:<Request (GET) http://httpbin.org/headers> not in cassette, sending to real server
The second time, you will see::
INFO:vcr.stubs:Playing response for <Request (GET) http://httpbin.org/headers> from cassette
If you set the loglevel to DEBUG, you will also get information about
which matchers didn't match. This can help you with debugging custom
matchers.

24
docs/index.rst Normal file
View File

@@ -0,0 +1,24 @@
.. include:: ../README.rst
Contents
========
.. toctree::
:maxdepth: 2
installation
usage
configuration
advanced
api
debugging
contributing
changelog
==================
Indices and tables
==================
* :ref:`genindex`
* :ref:`modindex`
* :ref:`search`

86
docs/installation.rst Normal file
View File

@@ -0,0 +1,86 @@
Installation
============
VCR.py is a package on `PyPI <https://pypi.python.org>`__, so you can install
with pip::
pip install vcrpy
Compatibility
-------------
VCR.py supports Python 2.7 and 3.4+, and
`pypy <http://pypy.org>`__.
The following HTTP libraries are supported:
- ``aiohttp``
- ``boto``
- ``boto3``
- ``http.client``
- ``httplib2``
- ``requests`` (both 1.x and 2.x versions)
- ``tornado.httpclient``
- ``urllib2``
- ``urllib3``
Speed
-----
VCR.py runs about 10x faster when `pyyaml <http://pyyaml.org>`__ can use the
`libyaml extensions <http://pyyaml.org/wiki/LibYAML>`__. In order for this to
work, libyaml needs to be available when pyyaml is built. Additionally the flag
is cached by pip, so you might need to explicitly avoid the cache when
rebuilding pyyaml.
1. Test if pyyaml is built with libyaml. This should work::
python -c 'from yaml import CLoader'
2. Install libyaml according to your Linux distribution, or using `Homebrew
<http://mxcl.github.com/homebrew/>`__ on Mac::
brew install libyaml # Mac with Homebrew
apt-get install libyaml-dev # Ubuntu
dnf install libyaml-devel # Fedora
3. Rebuild pyyaml with libyaml::
pip uninstall pyyaml
pip --no-cache-dir install pyyaml
Upgrade
-------
New Cassette Format
~~~~~~~~~~~~~~~~~~~
The cassette format has changed in *VCR.py 1.x*, the *VCR.py 0.x*
cassettes cannot be used with *VCR.py 1.x*. The easiest way to upgrade
is to simply delete your cassettes and re-record all of them. VCR.py
also provides a migration script that attempts to upgrade your 0.x
cassettes to the new 1.x format. To use it, run the following command::
python -m vcr.migration PATH
The PATH can be either a path to the directory with cassettes or the
path to a single cassette.
*Note*: Back up your cassettes files before migration. The migration
*should* only modify cassettes using the old 0.x format.
New serializer / deserializer API
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
If you made a custom serializer, you will need to update it to match the
new API in version 1.0.x
- Serializers now take dicts and return strings.
- Deserializers take strings and return dicts (instead of requests,
responses pair)
Ruby VCR compatibility
----------------------
VCR.py does not aim to match the format of the Ruby VCR YAML files.
Cassettes generated by Ruby's VCR are not compatible with VCR.py.

97
docs/usage.rst Normal file
View File

@@ -0,0 +1,97 @@
Usage
=====
.. code:: python
import vcr
import urllib2
with vcr.use_cassette('fixtures/vcr_cassettes/synopsis.yaml'):
response = urllib2.urlopen('http://www.iana.org/domains/reserved').read()
assert 'Example domains' in response
Run this test once, and VCR.py will record the HTTP request to
``fixtures/vcr_cassettes/synopsis.yml``. Run it again, and VCR.py will
replay the response from iana.org when the http request is made. This
test is now fast (no real HTTP requests are made anymore), deterministic
(the test will continue to pass, even if you are offline, or iana.org
goes down for maintenance) and accurate (the response will contain the
same headers and body you get from a real request).
You can also use VCR.py as a decorator. The same request above would
look like this:
.. code:: python
@vcr.use_cassette('fixtures/vcr_cassettes/synopsis.yaml')
def test_iana():
response = urllib2.urlopen('http://www.iana.org/domains/reserved').read()
assert 'Example domains' in response
When using the decorator version of ``use_cassette``, it is possible to
omit the path to the cassette file.
.. code:: python
@vcr.use_cassette()
def test_iana():
response = urllib2.urlopen('http://www.iana.org/domains/reserved').read()
assert 'Example domains' in response
In this case, the cassette file will be given the same name as the test
function, and it will be placed in the same directory as the file in
which the test is defined. See the Automatic Test Naming section below
for more details.
Record Modes
------------
VCR supports 4 record modes (with the same behavior as Ruby's VCR):
once
~~~~
- Replay previously recorded interactions.
- Record new interactions if there is no cassette file.
- Cause an error to be raised for new requests if there is a cassette
file.
It is similar to the new\_episodes record mode, but will prevent new,
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.
new\_episodes
~~~~~~~~~~~~~
- Record new interactions.
- Replay previously recorded interactions. It is similar to the once
record mode, but will always record new interactions, even if you
have an existing recorded one that is similar, but not identical.
This was the default behavior in versions < 0.3.0
none
~~~~
- Replay previously recorded interactions.
- Cause an error to be raised for any new requests. This is useful when
your code makes potentially dangerous HTTP requests. The none record
mode guarantees that no new HTTP requests will be made.
all
~~~
- Record new interactions.
- Never replay previously recorded interactions. This can be
temporarily used to force VCR to re-record a cassette (i.e. to ensure
the responses are not out of date) or can be used when you simply
want to log all HTTP requests.
Unittest Integration
--------------------
While it's possible to use the context manager or decorator forms with unittest,
there's also a ``VCRTestCase`` provided separately by `vcrpy-unittest
<https://github.com/agriffis/vcrpy-unittest>`__.

3
runtests.sh Executable file
View File

@@ -0,0 +1,3 @@
#!/bin/bash
REQUESTS_CA_BUNDLE=`python -m pytest_httpbin.certs` py.test $*

2
setup.cfg Normal file
View File

@@ -0,0 +1,2 @@
[bdist_wheel]
universal=1

View File

@@ -1,9 +1,12 @@
#!/usr/bin/env python
import sys
from setuptools import setup
from setuptools import setup, find_packages
from setuptools.command.test import test as TestCommand
long_description = open('README.rst', 'r').read()
class PyTest(TestCommand):
@@ -18,41 +21,52 @@ class PyTest(TestCommand):
errno = pytest.main(self.test_args)
sys.exit(errno)
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.1.2',
version='2.0.0',
description=(
"Automatically mock your HTTP interactions to simplify and "
"speed up testing"
),
long_description=long_description,
author='Kevin McCarthy',
author_email='me@kevinmccarthy.org',
url='https://github.com/kevin1024/vcrpy',
packages=[
'vcr',
'vcr.stubs',
'vcr.compat',
'vcr.persisters',
'vcr.serializers',
],
package_dir={
'vcr': 'vcr',
'vcr.stubs': 'vcr/stubs',
'vcr.compat': 'vcr/compat',
'vcr.persisters': 'vcr/persisters',
},
install_requires=['PyYAML', 'mock', 'six', 'contextlib2', 'wrapt'],
packages=find_packages(exclude=excluded_packages),
python_requires='>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*',
install_requires=install_requires,
license='MIT',
tests_require=['pytest', 'mock', 'pytest-localserver'],
cmdclass={'test': PyTest},
tests_require=['pytest', 'mock', 'pytest-httpbin'],
classifiers=[
'Development Status :: 4 - Beta',
'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',
],
]
)

View File

@@ -0,0 +1,33 @@
# flake8: noqa
import asyncio
import aiohttp
from aiohttp.test_utils import TestClient
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

View File

@@ -0,0 +1,179 @@
import contextlib
import pytest
asyncio = pytest.importorskip("asyncio")
aiohttp = pytest.importorskip("aiohttp")
import vcr # noqa: E402
from .aiohttp_utils import aiohttp_app, aiohttp_request # noqa: E402
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, 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"])
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'
headers = {'Content-Type': 'application/json'}
with vcr.use_cassette(str(tmpdir.join('json.yaml'))):
_, response_json = get(url, output='json', headers=headers)
with vcr.use_cassette(str(tmpdir.join('json.yaml'))) as cassette:
_, 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'
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'
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

View File

@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
'''Basic tests for cassettes'''
# coding=utf-8
# External imports
import os
@@ -9,74 +9,63 @@ from six.moves.urllib.request import urlopen
import vcr
def test_nonexistent_directory(tmpdir):
def test_nonexistent_directory(tmpdir, httpbin):
'''If we load a cassette in a nonexistent directory, it can save ok'''
# Check to make sure directory doesnt exist
assert not os.path.exists(str(tmpdir.join('nonexistent')))
# Run VCR to create dir and cassette file
with vcr.use_cassette(str(tmpdir.join('nonexistent', 'cassette.yml'))):
urlopen('http://httpbin.org/').read()
urlopen(httpbin.url).read()
# This should have made the file and the directory
assert os.path.exists(str(tmpdir.join('nonexistent', 'cassette.yml')))
def test_unpatch(tmpdir):
def test_unpatch(tmpdir, httpbin):
'''Ensure that our cassette gets unpatched when we're done'''
with vcr.use_cassette(str(tmpdir.join('unpatch.yaml'))) as cass:
urlopen('http://httpbin.org/').read()
urlopen(httpbin.url).read()
# Make the same request, and assert that we haven't served any more
# requests out of cache
urlopen('http://httpbin.org/').read()
urlopen(httpbin.url).read()
assert cass.play_count == 0
def test_basic_use(tmpdir):
'''
Copied from the docs
'''
with vcr.use_cassette('fixtures/vcr_cassettes/synopsis.yaml'):
response = urlopen(
'http://www.iana.org/domains/reserved'
).read()
assert b'Example domains' in response
def test_basic_json_use(tmpdir):
def test_basic_json_use(tmpdir, httpbin):
'''
Ensure you can load a json serialized cassette
'''
test_fixture = 'fixtures/vcr_cassettes/synopsis.json'
test_fixture = str(tmpdir.join('synopsis.json'))
with vcr.use_cassette(test_fixture, serializer='json'):
response = urlopen('http://httpbin.org/').read()
response = urlopen(httpbin.url).read()
assert b'difficult sometimes' in response
def test_patched_content(tmpdir):
def test_patched_content(tmpdir, httpbin):
'''
Ensure that what you pull from a cassette is what came from the
request
'''
with vcr.use_cassette(str(tmpdir.join('synopsis.yaml'))) as cass:
response = urlopen('http://httpbin.org/').read()
response = urlopen(httpbin.url).read()
assert cass.play_count == 0
with vcr.use_cassette(str(tmpdir.join('synopsis.yaml'))) as cass:
response2 = urlopen('http://httpbin.org/').read()
response2 = urlopen(httpbin.url).read()
assert cass.play_count == 1
cass._save(force=True)
with vcr.use_cassette(str(tmpdir.join('synopsis.yaml'))) as cass:
response3 = urlopen('http://httpbin.org/').read()
response3 = urlopen(httpbin.url).read()
assert cass.play_count == 1
assert response == response2
assert response2 == response3
def test_patched_content_json(tmpdir):
def test_patched_content_json(tmpdir, httpbin):
'''
Ensure that what you pull from a json cassette is what came from the
request
@@ -85,16 +74,16 @@ def test_patched_content_json(tmpdir):
testfile = str(tmpdir.join('synopsis.json'))
with vcr.use_cassette(testfile) as cass:
response = urlopen('http://httpbin.org/').read()
response = urlopen(httpbin.url).read()
assert cass.play_count == 0
with vcr.use_cassette(testfile) as cass:
response2 = urlopen('http://httpbin.org/').read()
response2 = urlopen(httpbin.url).read()
assert cass.play_count == 1
cass._save(force=True)
with vcr.use_cassette(testfile) as cass:
response3 = urlopen('http://httpbin.org/').read()
response3 = urlopen(httpbin.url).read()
assert cass.play_count == 1
assert response == response2

View File

@@ -1,11 +1,17 @@
import pytest
boto = pytest.importorskip("boto")
import boto
import boto.iam
from boto.s3.connection import S3Connection
from boto.s3.key import Key
from ConfigParser import DuplicateSectionError
import vcr
import boto # NOQA
import boto.iam # NOQA
from boto.s3.connection import S3Connection # NOQA
from boto.s3.key import Key # NOQA
import vcr # NOQA
try: # NOQA
from ConfigParser import DuplicateSectionError # NOQA
except ImportError: # NOQA
# python3
from configparser import DuplicateSectionError # NOQA
def test_boto_stubs(tmpdir):
with vcr.use_cassette(str(tmpdir.join('boto-stubs.yml'))):
@@ -14,45 +20,48 @@ def test_boto_stubs(tmpdir):
from boto.https_connection import CertValidatingHTTPSConnection
from vcr.stubs.boto_stubs import VCRCertValidatingHTTPSConnection
# Prove that the class was patched by the stub and that we can instantiate it.
assert CertValidatingHTTPSConnection is VCRCertValidatingHTTPSConnection
assert issubclass(CertValidatingHTTPSConnection, VCRCertValidatingHTTPSConnection)
CertValidatingHTTPSConnection('hostname.does.not.matter')
def test_boto_without_vcr():
s3_conn = S3Connection()
s3_bucket = s3_conn.get_bucket('boto-demo-1394171994') # a bucket you can access
s3_bucket = s3_conn.get_bucket('boto-demo-1394171994') # a bucket you can access
k = Key(s3_bucket)
k.key = 'test.txt'
k.set_contents_from_string('hello world i am a string')
def test_boto_medium_difficulty(tmpdir):
s3_conn = S3Connection()
s3_bucket = s3_conn.get_bucket('boto-demo-1394171994') # a bucket you can access
with vcr.use_cassette(str(tmpdir.join('boto-medium.yml'))) as cass:
s3_bucket = s3_conn.get_bucket('boto-demo-1394171994') # a bucket you can access
with vcr.use_cassette(str(tmpdir.join('boto-medium.yml'))):
k = Key(s3_bucket)
k.key = 'test.txt'
k.set_contents_from_string('hello world i am a string')
with vcr.use_cassette(str(tmpdir.join('boto-medium.yml'))) as cass:
with vcr.use_cassette(str(tmpdir.join('boto-medium.yml'))):
k = Key(s3_bucket)
k.key = 'test.txt'
k.set_contents_from_string('hello world i am a string')
def test_boto_hardcore_mode(tmpdir):
with vcr.use_cassette(str(tmpdir.join('boto-hardcore.yml'))) as cass:
with vcr.use_cassette(str(tmpdir.join('boto-hardcore.yml'))):
s3_conn = S3Connection()
s3_bucket = s3_conn.get_bucket('boto-demo-1394171994') # a bucket you can access
s3_bucket = s3_conn.get_bucket('boto-demo-1394171994') # a bucket you can access
k = Key(s3_bucket)
k.key = 'test.txt'
k.set_contents_from_string('hello world i am a string')
with vcr.use_cassette(str(tmpdir.join('boto-hardcore.yml'))) as cass:
with vcr.use_cassette(str(tmpdir.join('boto-hardcore.yml'))):
s3_conn = S3Connection()
s3_bucket = s3_conn.get_bucket('boto-demo-1394171994') # a bucket you can access
s3_bucket = s3_conn.get_bucket('boto-demo-1394171994') # a bucket you can access
k = Key(s3_bucket)
k.key = 'test.txt'
k.set_contents_from_string('hello world i am a string')
def test_boto_iam(tmpdir):
try:
boto.config.add_section('Boto')
@@ -63,10 +72,10 @@ def test_boto_iam(tmpdir):
# Ensure that boto uses CertValidatingHTTPSConnection
boto.config.set('Boto', 'https_validate_certificates', 'true')
with vcr.use_cassette(str(tmpdir.join('boto-iam.yml'))) as cass:
with vcr.use_cassette(str(tmpdir.join('boto-iam.yml'))):
iam_conn = boto.iam.connect_to_region('universal')
iam_conn.get_all_users()
with vcr.use_cassette(str(tmpdir.join('boto-iam.yml'))) as cass:
with vcr.use_cassette(str(tmpdir.join('boto-iam.yml'))):
iam_conn = boto.iam.connect_to_region('universal')
iam_conn.get_all_users()

View File

@@ -0,0 +1,67 @@
import pytest
boto3 = pytest.importorskip("boto3")
import boto3 # NOQA
import vcr # NOQA
bucket = 'boto3-demo-1337' # a bucket you can access
key = 'test/my_test.txt' # key with r+w access
content = 'hello world i am a string' # content to put in the test file
def test_boto_stubs(tmpdir):
with vcr.use_cassette(str(tmpdir.join('boto3-stubs.yml'))):
# Perform the imports within the patched context so that
# HTTPConnection, VerifiedHTTPSConnection refers to the patched version.
from botocore.vendored.requests.packages.urllib3.connectionpool import \
HTTPConnection, VerifiedHTTPSConnection
from vcr.stubs.boto3_stubs import VCRRequestsHTTPConnection, VCRRequestsHTTPSConnection
# Prove that the class was patched by the stub and that we can instantiate it.
assert issubclass(HTTPConnection, VCRRequestsHTTPConnection)
assert issubclass(VerifiedHTTPSConnection, VCRRequestsHTTPSConnection)
HTTPConnection('hostname.does.not.matter')
VerifiedHTTPSConnection('hostname.does.not.matter')
def test_boto3_without_vcr():
s3_resource = boto3.resource('s3')
b = s3_resource.Bucket(bucket)
b.put_object(Key=key, Body=content)
# retrieve content to check it
o = s3_resource.Object(bucket, key).get()
# decode for python3
assert content == o['Body'].read().decode('utf-8')
def test_boto_medium_difficulty(tmpdir):
s3_resource = boto3.resource('s3')
b = s3_resource.Bucket(bucket)
with vcr.use_cassette(str(tmpdir.join('boto3-medium.yml'))):
b.put_object(Key=key, Body=content)
o = s3_resource.Object(bucket, key).get()
assert content == o['Body'].read().decode('utf-8')
with vcr.use_cassette(str(tmpdir.join('boto3-medium.yml'))) as cass:
b.put_object(Key=key, Body=content)
o = s3_resource.Object(bucket, key).get()
assert content == o['Body'].read().decode('utf-8')
assert cass.all_played
def test_boto_hardcore_mode(tmpdir):
with vcr.use_cassette(str(tmpdir.join('boto3-hardcore.yml'))):
s3_resource = boto3.resource('s3')
b = s3_resource.Bucket(bucket)
b.put_object(Key=key, Body=content)
o = s3_resource.Object(bucket, key).get()
assert content == o['Body'].read().decode('utf-8')
with vcr.use_cassette(str(tmpdir.join('boto3-hardcore.yml'))) as cass:
s3_resource = boto3.resource('s3')
b = s3_resource.Bucket(bucket)
b.put_object(Key=key, Body=content)
o = s3_resource.Object(bucket, key).get()
assert content == o['Body'].read().decode('utf-8')
assert cass.all_played

View File

@@ -5,46 +5,46 @@ import vcr
from six.moves.urllib.request import urlopen
def test_set_serializer_default_config(tmpdir):
def test_set_serializer_default_config(tmpdir, httpbin):
my_vcr = vcr.VCR(serializer='json')
with my_vcr.use_cassette(str(tmpdir.join('test.json'))):
assert my_vcr.serializer == 'json'
urlopen('http://httpbin.org/get')
urlopen(httpbin.url + '/get')
with open(str(tmpdir.join('test.json'))) as f:
assert json.loads(f.read())
def test_default_set_cassette_library_dir(tmpdir):
def test_default_set_cassette_library_dir(tmpdir, httpbin):
my_vcr = vcr.VCR(cassette_library_dir=str(tmpdir.join('subdir')))
with my_vcr.use_cassette('test.json'):
urlopen('http://httpbin.org/get')
urlopen(httpbin.url + '/get')
assert os.path.exists(str(tmpdir.join('subdir').join('test.json')))
def test_override_set_cassette_library_dir(tmpdir):
def test_override_set_cassette_library_dir(tmpdir, httpbin):
my_vcr = vcr.VCR(cassette_library_dir=str(tmpdir.join('subdir')))
cld = str(tmpdir.join('subdir2'))
with my_vcr.use_cassette('test.json', cassette_library_dir=cld):
urlopen('http://httpbin.org/get')
urlopen(httpbin.url + '/get')
assert os.path.exists(str(tmpdir.join('subdir2').join('test.json')))
assert not os.path.exists(str(tmpdir.join('subdir').join('test.json')))
def test_override_match_on(tmpdir):
def test_override_match_on(tmpdir, httpbin):
my_vcr = vcr.VCR(match_on=['method'])
with my_vcr.use_cassette(str(tmpdir.join('test.json'))):
urlopen('http://httpbin.org/')
urlopen(httpbin.url)
with my_vcr.use_cassette(str(tmpdir.join('test.json'))) as cass:
urlopen('http://httpbin.org/get')
urlopen(httpbin.url + '/get')
assert len(cass) == 1
assert cass.play_count == 1

View File

@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
'''Basic tests about save behavior'''
# coding=utf-8
# External imports
import os
@@ -10,19 +10,19 @@ from six.moves.urllib.request import urlopen
import vcr
def test_disk_saver_nowrite(tmpdir):
def test_disk_saver_nowrite(tmpdir, httpbin):
'''
Ensure that when you close a cassette without changing it it doesn't
rewrite the file
'''
fname = str(tmpdir.join('synopsis.yaml'))
with vcr.use_cassette(fname) as cass:
urlopen('http://www.iana.org/domains/reserved').read()
urlopen(httpbin.url).read()
assert cass.play_count == 0
last_mod = os.path.getmtime(fname)
with vcr.use_cassette(fname) as cass:
urlopen('http://www.iana.org/domains/reserved').read()
urlopen(httpbin.url).read()
assert cass.play_count == 1
assert cass.dirty is False
last_mod2 = os.path.getmtime(fname)
@@ -30,14 +30,14 @@ def test_disk_saver_nowrite(tmpdir):
assert last_mod == last_mod2
def test_disk_saver_write(tmpdir):
def test_disk_saver_write(tmpdir, httpbin):
'''
Ensure that when you close a cassette after changing it it does
rewrite the file
'''
fname = str(tmpdir.join('synopsis.yaml'))
with vcr.use_cassette(fname) as cass:
urlopen('http://www.iana.org/domains/reserved').read()
urlopen(httpbin.url).read()
assert cass.play_count == 0
last_mod = os.path.getmtime(fname)
@@ -46,8 +46,8 @@ def test_disk_saver_write(tmpdir):
time.sleep(1)
with vcr.use_cassette(fname, record_mode='any') as cass:
urlopen('http://www.iana.org/domains/reserved').read()
urlopen('http://httpbin.org/').read()
urlopen(httpbin.url).read()
urlopen(httpbin.url + '/get').read()
assert cass.play_count == 1
assert cass.dirty
last_mod2 = os.path.getmtime(fname)

View File

@@ -1,8 +1,11 @@
import base64
import pytest
from six.moves.urllib.request import urlopen, Request
from six.moves.urllib.parse import urlencode
from six.moves.urllib.error import HTTPError
import vcr
import json
from assertions import assert_cassette_has_one_response, assert_is_json
def _request_with_auth(url, username, password):
@@ -15,15 +18,11 @@ def _request_with_auth(url, username, password):
def _find_header(cassette, header):
for request in cassette.requests:
for k in request.headers:
if header.lower() == k.lower():
return True
return False
return any(header in request.headers for request in cassette.requests)
def test_filter_basic_auth(tmpdir):
url = 'http://httpbin.org/basic-auth/user/passwd'
def test_filter_basic_auth(tmpdir, httpbin):
url = httpbin.url + '/basic-auth/user/passwd'
cass_file = str(tmpdir.join('basic_auth_filter.yaml'))
my_vcr = vcr.VCR(match_on=['uri', 'method', 'headers'])
# 2 requests, one with auth failure and one with auth success
@@ -45,8 +44,8 @@ def test_filter_basic_auth(tmpdir):
assert len(cass) == 2
def test_filter_querystring(tmpdir):
url = 'http://httpbin.org/?foo=bar'
def test_filter_querystring(tmpdir, httpbin):
url = httpbin.url + '/?foo=bar'
cass_file = str(tmpdir.join('filter_qs.yaml'))
with vcr.use_cassette(cass_file, filter_query_parameters=['foo']):
urlopen(url)
@@ -55,12 +54,36 @@ def test_filter_querystring(tmpdir):
assert 'foo' not in cass.requests[0].url
def test_filter_callback(tmpdir):
url = 'http://httpbin.org/get'
def test_filter_post_data(tmpdir, httpbin):
url = httpbin.url + '/post'
data = urlencode({'id': 'secret', 'foo': 'bar'}).encode('utf-8')
cass_file = str(tmpdir.join('filter_pd.yaml'))
with vcr.use_cassette(cass_file, filter_post_data_parameters=['id']):
urlopen(url, data)
with vcr.use_cassette(cass_file, filter_post_data_parameters=['id']) as cass:
assert b'id=secret' not in cass.requests[0].body
def test_filter_json_post_data(tmpdir, httpbin):
data = json.dumps({'id': 'secret', 'foo': 'bar'}).encode('utf-8')
request = Request(httpbin.url + '/post', data=data)
request.add_header('Content-Type', 'application/json')
cass_file = str(tmpdir.join('filter_jpd.yaml'))
with vcr.use_cassette(cass_file, filter_post_data_parameters=['id']):
urlopen(request)
with vcr.use_cassette(cass_file, filter_post_data_parameters=['id']) as cass:
assert b'"id": "secret"' not in cass.requests[0].body
def test_filter_callback(tmpdir, httpbin):
url = httpbin.url + '/get'
cass_file = str(tmpdir.join('basic_auth_filter.yaml'))
def before_record_cb(request):
if request.path != '/get':
return request
# Test the legacy keyword.
my_vcr = vcr.VCR(before_record=before_record_cb)
with my_vcr.use_cassette(cass_file, filter_headers=['authorization']) as cass:
@@ -71,3 +94,39 @@ def test_filter_callback(tmpdir):
with my_vcr.use_cassette(cass_file, filter_headers=['authorization']) as cass:
urlopen(url)
assert len(cass) == 0
def test_decompress_gzip(tmpdir, httpbin):
url = httpbin.url + '/gzip'
request = Request(url, headers={'Accept-Encoding': ['gzip, deflate']})
cass_file = str(tmpdir.join('gzip_response.yaml'))
with vcr.use_cassette(cass_file, decode_compressed_response=True):
urlopen(request)
with vcr.use_cassette(cass_file) as cass:
decoded_response = urlopen(url).read()
assert_cassette_has_one_response(cass)
assert_is_json(decoded_response)
def test_decompress_deflate(tmpdir, httpbin):
url = httpbin.url + '/deflate'
request = Request(url, headers={'Accept-Encoding': ['gzip, deflate']})
cass_file = str(tmpdir.join('deflate_response.yaml'))
with vcr.use_cassette(cass_file, decode_compressed_response=True):
urlopen(request)
with vcr.use_cassette(cass_file) as cass:
decoded_response = urlopen(url).read()
assert_cassette_has_one_response(cass)
assert_is_json(decoded_response)
def test_decompress_regular(tmpdir, httpbin):
"""Test that it doesn't try to decompress content that isn't compressed"""
url = httpbin.url + '/get'
cass_file = str(tmpdir.join('noncompressed_response.yaml'))
with vcr.use_cassette(cass_file, decode_compressed_response=True):
urlopen(url)
with vcr.use_cassette(cass_file) as cass:
resp = urlopen(url).read()
assert_cassette_has_one_response(cass)
assert_is_json(resp)

View File

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

View File

@@ -1,11 +1,12 @@
# -*- coding: utf-8 -*-
'''Integration tests with httplib2'''
# coding=utf-8
# 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
@@ -13,128 +14,146 @@ from assertions import assert_cassette_has_one_response
httplib2 = pytest.importorskip("httplib2")
@pytest.fixture(params=["https", "http"])
def scheme(request):
def http():
"""
Fixture that returns both http and https
Returns an httplib2 HTTP instance
with the certificate replaced by the httpbin one.
"""
return request.param
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(scheme, tmpdir):
def test_response_code(tmpdir, httpbin_both):
'''Ensure we can read a response code from a fetch'''
url = scheme + '://httpbin.org/'
with vcr.use_cassette(str(tmpdir.join('atts.yaml'))) as cass:
resp, _ = httplib2.Http().request(url)
url = httpbin_both.url
with vcr.use_cassette(str(tmpdir.join('atts.yaml'))):
resp, _ = http().request(url)
code = resp.status
with vcr.use_cassette(str(tmpdir.join('atts.yaml'))) as cass:
resp, _ = httplib2.Http().request(url)
with vcr.use_cassette(str(tmpdir.join('atts.yaml'))):
resp, _ = http().request(url)
assert code == resp.status
def test_random_body(scheme, tmpdir):
def test_random_body(httpbin_both, tmpdir):
'''Ensure we can read the content, and that it's served from cache'''
url = scheme + '://httpbin.org/bytes/1024'
with vcr.use_cassette(str(tmpdir.join('body.yaml'))) as cass:
_, content = httplib2.Http().request(url)
url = httpbin_both.url + '/bytes/1024'
with vcr.use_cassette(str(tmpdir.join('body.yaml'))):
_, content = http().request(url)
body = content
with vcr.use_cassette(str(tmpdir.join('body.yaml'))) as cass:
_, content = httplib2.Http().request(url)
with vcr.use_cassette(str(tmpdir.join('body.yaml'))):
_, content = http().request(url)
assert body == content
def test_response_headers(scheme, tmpdir):
def test_response_headers(tmpdir, httpbin_both):
'''Ensure we can get information from the response'''
url = scheme + '://httpbin.org/'
with vcr.use_cassette(str(tmpdir.join('headers.yaml'))) as cass:
resp, _ = httplib2.Http().request(url)
url = httpbin_both.url
with vcr.use_cassette(str(tmpdir.join('headers.yaml'))):
resp, _ = http().request(url)
headers = resp.items()
with vcr.use_cassette(str(tmpdir.join('headers.yaml'))) as cass:
resp, _ = httplib2.Http().request(url)
assert headers == resp.items()
with vcr.use_cassette(str(tmpdir.join('headers.yaml'))):
resp, _ = http().request(url)
assert set(headers) == set(resp.items())
def test_multiple_requests(scheme, tmpdir):
def test_effective_url(tmpdir, httpbin_both):
'''Ensure that the effective_url is captured'''
url = httpbin_both.url + '/redirect-to?url=/html'
with vcr.use_cassette(str(tmpdir.join('headers.yaml'))):
resp, _ = http().request(url)
effective_url = resp['content-location']
assert effective_url == httpbin_both + '/html'
with vcr.use_cassette(str(tmpdir.join('headers.yaml'))):
resp, _ = http().request(url)
assert effective_url == resp['content-location']
def test_multiple_requests(tmpdir, httpbin_both):
'''Ensure that we can cache multiple requests'''
urls = [
scheme + '://httpbin.org/',
scheme + '://httpbin.org/',
scheme + '://httpbin.org/get',
scheme + '://httpbin.org/bytes/1024'
httpbin_both.url,
httpbin_both.url,
httpbin_both.url + '/get',
httpbin_both.url + '/bytes/1024',
]
with vcr.use_cassette(str(tmpdir.join('multiple.yaml'))) as cass:
[httplib2.Http().request(url) for url in urls]
[http().request(url) for url in urls]
assert len(cass) == len(urls)
def test_get_data(scheme, tmpdir):
def test_get_data(tmpdir, httpbin_both):
'''Ensure that it works with query data'''
data = urlencode({'some': 1, 'data': 'here'})
url = scheme + '://httpbin.org/get?' + data
with vcr.use_cassette(str(tmpdir.join('get_data.yaml'))) as cass:
_, res1 = httplib2.Http().request(url)
url = httpbin_both.url + '/get?' + data
with vcr.use_cassette(str(tmpdir.join('get_data.yaml'))):
_, res1 = http().request(url)
with vcr.use_cassette(str(tmpdir.join('get_data.yaml'))) as cass:
_, res2 = httplib2.Http().request(url)
with vcr.use_cassette(str(tmpdir.join('get_data.yaml'))):
_, res2 = http().request(url)
assert res1 == res2
def test_post_data(scheme, tmpdir):
def test_post_data(tmpdir, httpbin_both):
'''Ensure that it works when posting data'''
data = urlencode({'some': 1, 'data': 'here'})
url = scheme + '://httpbin.org/post'
with vcr.use_cassette(str(tmpdir.join('post_data.yaml'))) as cass:
_, res1 = httplib2.Http().request(url, "POST", data)
url = httpbin_both.url + '/post'
with vcr.use_cassette(str(tmpdir.join('post_data.yaml'))):
_, res1 = http().request(url, "POST", data)
with vcr.use_cassette(str(tmpdir.join('post_data.yaml'))) as cass:
_, res2 = httplib2.Http().request(url, "POST", data)
_, res2 = http().request(url, "POST", data)
assert res1 == res2
assert_cassette_has_one_response(cass)
def test_post_unicode_data(scheme, tmpdir):
def test_post_unicode_data(tmpdir, httpbin_both):
'''Ensure that it works when posting unicode data'''
data = urlencode({'snowman': u''.encode('utf-8')})
url = scheme + '://httpbin.org/post'
with vcr.use_cassette(str(tmpdir.join('post_data.yaml'))) as cass:
_, res1 = httplib2.Http().request(url, "POST", data)
url = httpbin_both.url + '/post'
with vcr.use_cassette(str(tmpdir.join('post_data.yaml'))):
_, res1 = http().request(url, "POST", data)
with vcr.use_cassette(str(tmpdir.join('post_data.yaml'))) as cass:
_, res2 = httplib2.Http().request(url, "POST", data)
_, res2 = http().request(url, "POST", data)
assert res1 == res2
assert_cassette_has_one_response(cass)
def test_cross_scheme(tmpdir):
def test_cross_scheme(tmpdir, httpbin, httpbin_secure):
'''Ensure that requests between schemes are treated separately'''
# First fetch a url under https, and then again under https and then
# ensure that we haven't served anything out of cache, and we have two
# requests / response pairs in the cassette
with vcr.use_cassette(str(tmpdir.join('cross_scheme.yaml'))) as cass:
httplib2.Http().request('https://httpbin.org/')
httplib2.Http().request('http://httpbin.org/')
http().request(httpbin_secure.url)
http().request(httpbin.url)
assert len(cass) == 2
assert cass.play_count == 0
def test_decorator(scheme, tmpdir):
def test_decorator(tmpdir, httpbin_both):
'''Test the decorator version of VCR.py'''
url = scheme + '://httpbin.org/'
url = httpbin_both.url
@vcr.use_cassette(str(tmpdir.join('atts.yaml')))
def inner1():
resp, _ = httplib2.Http().request(url)
resp, _ = http().request(url)
return resp['status']
@vcr.use_cassette(str(tmpdir.join('atts.yaml')))
def inner2():
resp, _ = httplib2.Http().request(url)
resp, _ = http().request(url)
return resp['status']
assert inner1() == inner2()

View File

@@ -1,55 +1,73 @@
import base64
import pytest
from six.moves.urllib.request import urlopen, Request
from six.moves.urllib.error import HTTPError
from six.moves.urllib.request import urlopen
import socket
from contextlib import contextmanager
import vcr
def test_ignore_localhost(tmpdir, httpserver):
httpserver.serve_content('Hello!')
cass_file = str(tmpdir.join('filter_qs.yaml'))
with vcr.use_cassette(cass_file, ignore_localhost=True) as cass:
urlopen(httpserver.url)
assert len(cass) == 0
urlopen('http://httpbin.org')
assert len(cass) == 1
@contextmanager
def overridden_dns(overrides):
"""
Monkeypatch socket.getaddrinfo() to override DNS lookups (name will resolve
to address)
"""
real_getaddrinfo = socket.getaddrinfo
def fake_getaddrinfo(*args, **kwargs):
if args[0] in overrides:
address = overrides[args[0]]
return [(2, 1, 6, '', (address, args[1]))]
return real_getaddrinfo(*args, **kwargs)
socket.getaddrinfo = fake_getaddrinfo
yield
socket.getaddrinfo = real_getaddrinfo
def test_ignore_httpbin(tmpdir, httpserver):
httpserver.serve_content('Hello!')
cass_file = str(tmpdir.join('filter_qs.yaml'))
with vcr.use_cassette(
cass_file,
ignore_hosts=['httpbin.org']
) as cass:
urlopen('http://httpbin.org')
assert len(cass) == 0
urlopen(httpserver.url)
assert len(cass) == 1
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:{}/'.format(httpbin.port))
assert len(cass) == 0
urlopen('http://httpbin.org:{}/'.format(httpbin.port))
assert len(cass) == 1
def test_ignore_localhost_and_httpbin(tmpdir, httpserver):
httpserver.serve_content('Hello!')
cass_file = str(tmpdir.join('filter_qs.yaml'))
with vcr.use_cassette(
cass_file,
ignore_hosts=['httpbin.org'],
ignore_localhost=True
) as cass:
urlopen('http://httpbin.org')
urlopen(httpserver.url)
assert len(cass) == 0
def test_ignore_httpbin(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_hosts=['httpbin.org']
) as cass:
urlopen('http://httpbin.org:{}/'.format(httpbin.port))
assert len(cass) == 0
urlopen('http://localhost:{}/'.format(httpbin.port))
assert len(cass) == 1
def test_ignore_localhost_twice(tmpdir, httpserver):
httpserver.serve_content('Hello!')
cass_file = str(tmpdir.join('filter_qs.yaml'))
with vcr.use_cassette(cass_file, ignore_localhost=True) as cass:
urlopen(httpserver.url)
assert len(cass) == 0
urlopen('http://httpbin.org')
assert len(cass) == 1
with vcr.use_cassette(cass_file, ignore_localhost=True) as cass:
assert len(cass) == 1
urlopen(httpserver.url)
urlopen('http://httpbin.org')
assert len(cass) == 1
def test_ignore_localhost_and_httpbin(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_hosts=['httpbin.org'],
ignore_localhost=True
) as cass:
urlopen('http://httpbin.org:{}'.format(httpbin.port))
urlopen('http://localhost:{}'.format(httpbin.port))
assert len(cass) == 0
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:{}'.format(httpbin.port))
assert len(cass) == 0
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:{}'.format(httpbin.port))
urlopen('http://httpbin.org:{}'.format(httpbin.port))
assert len(cass) == 1

View File

@@ -6,15 +6,21 @@ from six.moves.urllib.request import urlopen
DEFAULT_URI = 'http://httpbin.org/get?p1=q1&p2=q2' # base uri for testing
def _replace_httpbin(uri, httpbin, httpbin_secure):
return uri.replace('http://httpbin.org', httpbin.url).replace('https://httpbin.org', httpbin_secure.url)
@pytest.fixture
def cassette(tmpdir):
def cassette(tmpdir, httpbin, httpbin_secure):
"""
Helper fixture used to prepare the cassete
returns path to the recorded cassette
"""
default_uri = _replace_httpbin(DEFAULT_URI, httpbin, httpbin_secure)
cassette_path = str(tmpdir.join('test.yml'))
with vcr.use_cassette(cassette_path, record_mode='all'):
urlopen(DEFAULT_URI)
urlopen(default_uri)
return cassette_path
@@ -28,20 +34,22 @@ def cassette(tmpdir):
('host',
'https://httpbin.org/post?a=b',
'http://google.com/get?p1=q1&p2=q2'),
('port',
'https://google.com:80/post?a=b',
'http://httpbin.org:5000/get?p1=q1&p2=q2'),
('path',
'https://google.com/get?a=b',
'http://httpbin.org/post?p1=q1&p2=q2'),
('query',
'https://google.com/get?p2=q2&p1=q1',
'http://httpbin.org/get?p1=q1&a=b')
])
def test_matchers(cassette, matcher, matching_uri, not_matching_uri):
])
def test_matchers(httpbin, httpbin_secure, cassette, matcher, matching_uri, not_matching_uri):
matching_uri = _replace_httpbin(matching_uri, httpbin, httpbin_secure)
not_matching_uri = _replace_httpbin(not_matching_uri, httpbin, httpbin_secure)
default_uri = _replace_httpbin(DEFAULT_URI, httpbin, httpbin_secure)
# play cassette with default uri
with vcr.use_cassette(cassette, match_on=[matcher]) as cass:
urlopen(DEFAULT_URI)
urlopen(default_uri)
assert cass.play_count == 1
# play cassette with matching on uri
@@ -55,7 +63,9 @@ def test_matchers(cassette, matcher, matching_uri, not_matching_uri):
urlopen(not_matching_uri)
def test_method_matcher(cassette):
def test_method_matcher(cassette, httpbin, httpbin_secure):
default_uri = _replace_httpbin(DEFAULT_URI, httpbin, httpbin_secure)
# play cassette with matching on method
with vcr.use_cassette(cassette, match_on=['method']) as cass:
urlopen('https://google.com/get?a=b')
@@ -65,7 +75,7 @@ def test_method_matcher(cassette):
with pytest.raises(vcr.errors.CannotOverwriteExistingCassetteException):
with vcr.use_cassette(cassette, match_on=['method']) as cass:
# is a POST request
urlopen(DEFAULT_URI, data=b'')
urlopen(default_uri, data=b'')
@pytest.mark.parametrize("uri", [
@@ -73,7 +83,10 @@ def test_method_matcher(cassette):
'http://httpbin.org/get?p2=q2&p1=q1',
'http://httpbin.org/get?p2=q2&p1=q1',
])
def test_default_matcher_matches(cassette, uri):
def test_default_matcher_matches(cassette, uri, httpbin, httpbin_secure):
uri = _replace_httpbin(uri, httpbin, httpbin_secure)
with vcr.use_cassette(cassette) as cass:
urlopen(uri)
assert cass.play_count == 1
@@ -82,18 +95,19 @@ def test_default_matcher_matches(cassette, uri):
@pytest.mark.parametrize("uri", [
'https://httpbin.org/get?p1=q1&p2=q2',
'http://google.com/get?p1=q1&p2=q2',
'http://httpbin.org:5000/get?p1=q1&p2=q2',
'http://httpbin.org/post?p1=q1&p2=q2',
'http://httpbin.org/get?p1=q1&a=b'
])
def test_default_matcher_does_not_match(cassette, uri):
def test_default_matcher_does_not_match(cassette, uri, httpbin, httpbin_secure):
uri = _replace_httpbin(uri, httpbin, httpbin_secure)
with pytest.raises(vcr.errors.CannotOverwriteExistingCassetteException):
with vcr.use_cassette(cassette):
urlopen(uri)
def test_default_matcher_does_not_match_on_method(cassette):
def test_default_matcher_does_not_match_on_method(cassette, httpbin, httpbin_secure):
default_uri = _replace_httpbin(DEFAULT_URI, httpbin, httpbin_secure)
with pytest.raises(vcr.errors.CannotOverwriteExistingCassetteException):
with vcr.use_cassette(cassette):
# is a POST request
urlopen(DEFAULT_URI, data=b'')
urlopen(default_uri, data=b'')

View File

@@ -3,18 +3,18 @@ import vcr
from six.moves.urllib.request import urlopen
def test_making_extra_request_raises_exception(tmpdir):
def test_making_extra_request_raises_exception(tmpdir, httpbin):
# make two requests in the first request that are considered
# identical (since the match is based on method)
with vcr.use_cassette(str(tmpdir.join('test.json')), match_on=['method']):
urlopen('http://httpbin.org/status/200')
urlopen('http://httpbin.org/status/201')
urlopen(httpbin.url + '/status/200')
urlopen(httpbin.url + '/status/201')
# Now, try to make three requests. The first two should return the
# correct status codes in order, and the third should raise an
# exception.
with vcr.use_cassette(str(tmpdir.join('test.json')), match_on=['method']):
assert urlopen('http://httpbin.org/status/200').getcode() == 200
assert urlopen('http://httpbin.org/status/201').getcode() == 201
assert urlopen(httpbin.url + '/status/200').getcode() == 200
assert urlopen(httpbin.url + '/status/201').getcode() == 201
with pytest.raises(Exception):
urlopen('http://httpbin.org/status/200')
urlopen(httpbin.url + '/status/200')

View 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

View File

@@ -1,65 +1,64 @@
import os
import pytest
import vcr
from six.moves.urllib.request import urlopen
def test_once_record_mode(tmpdir):
def test_once_record_mode(tmpdir, httpbin):
testfile = str(tmpdir.join('recordmode.yml'))
with vcr.use_cassette(testfile, record_mode="once"):
# cassette file doesn't exist, so create.
response = urlopen('http://httpbin.org/').read()
urlopen(httpbin.url).read()
with vcr.use_cassette(testfile, record_mode="once") as cass:
with vcr.use_cassette(testfile, record_mode="once"):
# make the same request again
response = urlopen('http://httpbin.org/').read()
urlopen(httpbin.url).read()
# the first time, it's played from the cassette.
# but, try to access something else from the same cassette, and an
# exception is raised.
with pytest.raises(Exception):
response = urlopen('http://httpbin.org/get').read()
urlopen(httpbin.url + '/get').read()
def test_once_record_mode_two_times(tmpdir):
def test_once_record_mode_two_times(tmpdir, httpbin):
testfile = str(tmpdir.join('recordmode.yml'))
with vcr.use_cassette(testfile, record_mode="once"):
# get two of the same file
response1 = urlopen('http://httpbin.org/').read()
response2 = urlopen('http://httpbin.org/').read()
urlopen(httpbin.url).read()
urlopen(httpbin.url).read()
with vcr.use_cassette(testfile, record_mode="once") as cass:
with vcr.use_cassette(testfile, record_mode="once"):
# do it again
response = urlopen('http://httpbin.org/').read()
response = urlopen('http://httpbin.org/').read()
urlopen(httpbin.url).read()
urlopen(httpbin.url).read()
def test_once_mode_three_times(tmpdir):
def test_once_mode_three_times(tmpdir, httpbin):
testfile = str(tmpdir.join('recordmode.yml'))
with vcr.use_cassette(testfile, record_mode="once"):
# get three of the same file
response1 = urlopen('http://httpbin.org/').read()
response2 = urlopen('http://httpbin.org/').read()
response2 = urlopen('http://httpbin.org/').read()
urlopen(httpbin.url).read()
urlopen(httpbin.url).read()
urlopen(httpbin.url).read()
def test_new_episodes_record_mode(tmpdir):
def test_new_episodes_record_mode(tmpdir, httpbin):
testfile = str(tmpdir.join('recordmode.yml'))
with vcr.use_cassette(testfile, record_mode="new_episodes"):
# cassette file doesn't exist, so create.
response = urlopen('http://httpbin.org/').read()
urlopen(httpbin.url).read()
with vcr.use_cassette(testfile, record_mode="new_episodes") as cass:
# make the same request again
response = urlopen('http://httpbin.org/').read()
urlopen(httpbin.url).read()
# all responses have been played
assert cass.all_played
# in the "new_episodes" record mode, we can add more requests to
# a cassette without repurcussions.
response = urlopen('http://httpbin.org/get').read()
urlopen(httpbin.url + '/get').read()
# one of the responses has been played
assert cass.play_count == 1
@@ -72,20 +71,45 @@ def test_new_episodes_record_mode(tmpdir):
assert len(cass.responses) == 2
def test_all_record_mode(tmpdir):
def test_new_episodes_record_mode_two_times(tmpdir, httpbin):
testfile = str(tmpdir.join('recordmode.yml'))
url = httpbin.url + '/bytes/1024'
with vcr.use_cassette(testfile, record_mode="new_episodes"):
# cassette file doesn't exist, so create.
original_first_response = urlopen(url).read()
with vcr.use_cassette(testfile, record_mode="new_episodes"):
# make the same request again
assert urlopen(url).read() == original_first_response
# in the "new_episodes" record mode, we can add the same request
# to the cassette without repercussions
original_second_response = urlopen(url).read()
with vcr.use_cassette(testfile, record_mode="once"):
# make the same request again
assert urlopen(url).read() == original_first_response
assert urlopen(url).read() == original_second_response
# now that we are back in once mode, this should raise
# an error.
with pytest.raises(Exception):
urlopen(url).read()
def test_all_record_mode(tmpdir, httpbin):
testfile = str(tmpdir.join('recordmode.yml'))
with vcr.use_cassette(testfile, record_mode="all"):
# cassette file doesn't exist, so create.
response = urlopen('http://httpbin.org/').read()
urlopen(httpbin.url).read()
with vcr.use_cassette(testfile, record_mode="all") as cass:
# make the same request again
response = urlopen('http://httpbin.org/').read()
urlopen(httpbin.url).read()
# in the "all" record mode, we can add more requests to
# a cassette without repurcussions.
response = urlopen('http://httpbin.org/get').read()
urlopen(httpbin.url + '/get').read()
# The cassette was never actually played, even though it existed.
# that's because, in "all" mode, the requests all go directly to
@@ -93,26 +117,26 @@ def test_all_record_mode(tmpdir):
assert cass.play_count == 0
def test_none_record_mode(tmpdir):
def test_none_record_mode(tmpdir, httpbin):
# Cassette file doesn't exist, yet we are trying to make a request.
# raise hell.
testfile = str(tmpdir.join('recordmode.yml'))
with vcr.use_cassette(testfile, record_mode="none"):
with pytest.raises(Exception):
response = urlopen('http://httpbin.org/').read()
urlopen(httpbin.url).read()
def test_none_record_mode_with_existing_cassette(tmpdir):
def test_none_record_mode_with_existing_cassette(tmpdir, httpbin):
# create a cassette file
testfile = str(tmpdir.join('recordmode.yml'))
with vcr.use_cassette(testfile, record_mode="all"):
response = urlopen('http://httpbin.org/').read()
urlopen(httpbin.url).read()
# play from cassette file
with vcr.use_cassette(testfile, record_mode="none") as cass:
response = urlopen('http://httpbin.org/').read()
urlopen(httpbin.url).read()
assert cass.play_count == 1
# but if I try to hit the net, raise an exception.
with pytest.raises(Exception):
response = urlopen('http://httpbin.org/get').read()
urlopen(httpbin.url + '/get').read()

View File

@@ -10,27 +10,27 @@ def false_matcher(r1, r2):
return False
def test_registered_true_matcher(tmpdir):
def test_registered_true_matcher(tmpdir, httpbin):
my_vcr = vcr.VCR()
my_vcr.register_matcher('true', true_matcher)
testfile = str(tmpdir.join('test.yml'))
with my_vcr.use_cassette(testfile, match_on=['true']) as cass:
with my_vcr.use_cassette(testfile, match_on=['true']):
# These 2 different urls are stored as the same request
urlopen('http://httpbin.org/')
urlopen('https://httpbin.org/get')
urlopen(httpbin.url)
urlopen(httpbin.url + '/get')
with my_vcr.use_cassette(testfile, match_on=['true']) as cass:
with my_vcr.use_cassette(testfile, match_on=['true']):
# I can get the response twice even though I only asked for it once
urlopen('http://httpbin.org/get')
urlopen('https://httpbin.org/get')
urlopen(httpbin.url + '/get')
urlopen(httpbin.url + '/get')
def test_registered_false_matcher(tmpdir):
def test_registered_false_matcher(tmpdir, httpbin):
my_vcr = vcr.VCR()
my_vcr.register_matcher('false', false_matcher)
testfile = str(tmpdir.join('test.yml'))
with my_vcr.use_cassette(testfile, match_on=['false']) as cass:
# These 2 different urls are stored as different requests
urlopen('http://httpbin.org/')
urlopen('https://httpbin.org/get')
urlopen(httpbin.url)
urlopen(httpbin.url + '/get')
assert len(cass) == 2

View 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

View File

@@ -10,7 +10,7 @@ class MockSerializer(object):
def deserialize(self, cassette_string):
self.serialize_count += 1
self.cassette_string = cassette_string
return {'interactions':[]}
return {'interactions': []}
def serialize(self, cassette_dict):
self.deserialize_count += 1

View File

@@ -2,21 +2,18 @@ import vcr
from six.moves.urllib.request import urlopen
def test_recorded_request_uri_with_redirected_request(tmpdir):
def test_recorded_request_uri_with_redirected_request(tmpdir, httpbin):
with vcr.use_cassette(str(tmpdir.join('test.yml'))) as cass:
assert len(cass) == 0
urlopen('http://httpbin.org/redirect/3')
assert cass.requests[0].uri == 'http://httpbin.org/redirect/3'
assert cass.requests[3].uri == 'http://httpbin.org/get'
urlopen(httpbin.url + '/redirect/3')
assert cass.requests[0].uri == httpbin.url + '/redirect/3'
assert cass.requests[3].uri == httpbin.url + '/get'
assert len(cass) == 4
def test_records_multiple_header_values(tmpdir, httpserver):
httpserver.serve_content('Hello!', headers=[('foo', 'bar'), ('foo', 'baz')])
def test_records_multiple_header_values(tmpdir, httpbin):
with vcr.use_cassette(str(tmpdir.join('test.yml'))) as cass:
assert len(cass) == 0
urlopen(httpserver.url)
urlopen(httpbin.url + '/response-headers?foo=bar&foo=baz')
assert len(cass) == 1
assert cass.responses[0]['headers']['foo'] == ['bar', 'baz']

View File

@@ -1,29 +1,18 @@
# -*- coding: utf-8 -*-
'''Test requests' interaction with vcr'''
# coding=utf-8
import os
import platform
import pytest
import sys
import vcr
from assertions import (
assert_cassette_empty,
assert_cassette_has_one_response,
assert_is_json
)
from assertions import assert_cassette_empty, assert_is_json
requests = pytest.importorskip("requests")
from requests.exceptions import ConnectionError # noqa E402
@pytest.fixture(params=["https", "http"])
def scheme(request):
"""
Fixture that returns both http and https
"""
return request.param
def test_status_code(scheme, tmpdir):
def test_status_code(httpbin_both, tmpdir):
'''Ensure that we can read the status code'''
url = scheme + '://httpbin.org/'
url = httpbin_both.url + '/'
with vcr.use_cassette(str(tmpdir.join('atts.yaml'))):
status_code = requests.get(url).status_code
@@ -31,9 +20,9 @@ def test_status_code(scheme, tmpdir):
assert status_code == requests.get(url).status_code
def test_headers(scheme, tmpdir):
def test_headers(httpbin_both, tmpdir):
'''Ensure that we can read the headers back'''
url = scheme + '://httpbin.org/'
url = httpbin_both + '/'
with vcr.use_cassette(str(tmpdir.join('headers.yaml'))):
headers = requests.get(url).headers
@@ -41,9 +30,9 @@ def test_headers(scheme, tmpdir):
assert headers == requests.get(url).headers
def test_body(tmpdir, scheme):
def test_body(tmpdir, httpbin_both):
'''Ensure the responses are all identical enough'''
url = scheme + '://httpbin.org/bytes/1024'
url = httpbin_both + '/bytes/1024'
with vcr.use_cassette(str(tmpdir.join('body.yaml'))):
content = requests.get(url).content
@@ -51,10 +40,33 @@ def test_body(tmpdir, scheme):
assert content == requests.get(url).content
def test_auth(tmpdir, scheme):
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'
with vcr.use_cassette(str(tmpdir.join('url.yaml'))):
effective_url = requests.get(url).url
assert effective_url == httpbin_both.url + '/html'
with vcr.use_cassette(str(tmpdir.join('url.yaml'))):
assert effective_url == requests.get(url).url
def test_auth(tmpdir, httpbin_both):
'''Ensure that we can handle basic auth'''
auth = ('user', 'passwd')
url = scheme + '://httpbin.org/basic-auth/user/passwd'
url = httpbin_both + '/basic-auth/user/passwd'
with vcr.use_cassette(str(tmpdir.join('auth.yaml'))):
one = requests.get(url, auth=auth)
@@ -64,10 +76,10 @@ def test_auth(tmpdir, scheme):
assert one.status_code == two.status_code
def test_auth_failed(tmpdir, scheme):
def test_auth_failed(tmpdir, httpbin_both):
'''Ensure that we can save failed auth statuses'''
auth = ('user', 'wrongwrongwrong')
url = scheme + '://httpbin.org/basic-auth/user/passwd'
url = httpbin_both + '/basic-auth/user/passwd'
with vcr.use_cassette(str(tmpdir.join('auth-failed.yaml'))) as cass:
# Ensure that this is empty to begin with
assert_cassette_empty(cass)
@@ -77,10 +89,10 @@ def test_auth_failed(tmpdir, scheme):
assert one.status_code == two.status_code == 401
def test_post(tmpdir, scheme):
def test_post(tmpdir, httpbin_both):
'''Ensure that we can post and cache the results'''
data = {'key1': 'value1', 'key2': 'value2'}
url = scheme + '://httpbin.org/post'
url = httpbin_both + '/post'
with vcr.use_cassette(str(tmpdir.join('requests.yaml'))):
req1 = requests.post(url, data).content
@@ -90,9 +102,42 @@ def test_post(tmpdir, scheme):
assert req1 == req2
def test_redirects(tmpdir, scheme):
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.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)
with vcr.use_cassette(str(tmpdir.join('requests.yaml'))):
req2 = requests.post(url, data2).content
assert req1 == req2
def test_redirects(tmpdir, httpbin_both):
'''Ensure that we can handle redirects'''
url = scheme + '://httpbin.org/redirect-to?url=bytes/1024'
url = httpbin_both + '/redirect-to?url=bytes/1024'
with vcr.use_cassette(str(tmpdir.join('requests.yaml'))):
content = requests.get(url).content
@@ -104,24 +149,24 @@ def test_redirects(tmpdir, scheme):
assert cass.play_count == 2
def test_cross_scheme(tmpdir, scheme):
def test_cross_scheme(tmpdir, httpbin_secure, httpbin):
'''Ensure that requests between schemes are treated separately'''
# First fetch a url under http, and then again under https and then
# ensure that we haven't served anything out of cache, and we have two
# requests / response pairs in the cassette
with vcr.use_cassette(str(tmpdir.join('cross_scheme.yaml'))) as cass:
requests.get('https://httpbin.org/')
requests.get('http://httpbin.org/')
requests.get(httpbin_secure + '/')
requests.get(httpbin + '/')
assert cass.play_count == 0
assert len(cass) == 2
def test_gzip(tmpdir, scheme):
def test_gzip(tmpdir, httpbin_both):
'''
Ensure that requests (actually urllib3) is able to automatically decompress
the response body
'''
url = scheme + '://httpbin.org/gzip'
url = httpbin_both + '/gzip'
response = requests.get(url)
with vcr.use_cassette(str(tmpdir.join('gzip.yaml'))):
@@ -132,7 +177,7 @@ def test_gzip(tmpdir, scheme):
assert_is_json(response.content)
def test_session_and_connection_close(tmpdir, scheme):
def test_session_and_connection_close(tmpdir, httpbin):
'''
This tests the issue in https://github.com/kevin1024/vcrpy/issues/48
@@ -143,29 +188,29 @@ def test_session_and_connection_close(tmpdir, scheme):
with vcr.use_cassette(str(tmpdir.join('session_connection_closed.yaml'))):
session = requests.session()
session.get('http://httpbin.org/get', headers={'Connection': 'close'})
session.get('http://httpbin.org/get', headers={'Connection': 'close'})
session.get(httpbin + '/get', headers={'Connection': 'close'})
session.get(httpbin + '/get', headers={'Connection': 'close'})
def test_https_with_cert_validation_disabled(tmpdir):
def test_https_with_cert_validation_disabled(tmpdir, httpbin_secure):
with vcr.use_cassette(str(tmpdir.join('cert_validation_disabled.yaml'))):
requests.get('https://httpbin.org', verify=False)
requests.get(httpbin_secure.url, verify=False)
def test_session_can_make_requests_after_requests_unpatched(tmpdir):
def test_session_can_make_requests_after_requests_unpatched(tmpdir, httpbin):
with vcr.use_cassette(str(tmpdir.join('test_session_after_unpatched.yaml'))):
session = requests.session()
session.get('http://httpbin.org/get')
session.get(httpbin + '/get')
with vcr.use_cassette(str(tmpdir.join('test_session_after_unpatched.yaml'))):
session = requests.session()
session.get('http://httpbin.org/get')
session.get(httpbin + '/get')
session.get('http://httpbin.org/status/200')
session.get(httpbin + '/status/200')
def test_session_created_before_use_cassette_is_patched(tmpdir, scheme):
url = scheme + '://httpbin.org/bytes/1024'
def test_session_created_before_use_cassette_is_patched(tmpdir, httpbin_both):
url = httpbin_both + '/bytes/1024'
# Record arbitrary, random data to the cassette
with vcr.use_cassette(str(tmpdir.join('session_created_outside.yaml'))):
session = requests.session()
@@ -174,20 +219,20 @@ def test_session_created_before_use_cassette_is_patched(tmpdir, scheme):
# Create a session outside of any cassette context manager
session = requests.session()
# Make a request to make sure that a connectionpool is instantiated
session.get(scheme + '://httpbin.org/get')
session.get(httpbin_both + '/get')
with vcr.use_cassette(str(tmpdir.join('session_created_outside.yaml'))):
# These should only be the same if the patching succeeded.
assert session.get(url).content == body
def test_nested_cassettes_with_session_created_before_nesting(scheme, tmpdir):
def test_nested_cassettes_with_session_created_before_nesting(httpbin_both, tmpdir):
'''
This tests ensures that a session that was created while one cassette was
active is patched to the use the responses of a second cassette when it
is enabled.
'''
url = scheme + '://httpbin.org/bytes/1024'
url = httpbin_both + '/bytes/1024'
with vcr.use_cassette(str(tmpdir.join('first_nested.yaml'))):
session = requests.session()
first_body = session.get(url).content
@@ -203,5 +248,37 @@ def test_nested_cassettes_with_session_created_before_nesting(scheme, tmpdir):
assert session.get(url).content == third_body
# Make sure that the session can now get content normally.
session.get('http://www.reddit.com')
assert 'User-agent' in session.get(httpbin_both.url + '/robots.txt').text
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, 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')),
match_on=('method', 'scheme', 'host', 'port', 'path', 'query', 'body')) as cass:
with open('tox.ini', 'rb') as f:
tox_content = f.read()
assert cass.requests[0].body.read() == tox_content
with open('tox.ini', 'rb') as f:
new_response = requests.post(url, f).content
assert original_response == new_response
def test_filter_post_params(tmpdir, httpbin_both):
'''
This tests the issue in https://github.com/kevin1024/vcrpy/issues/158
Ensure that a post request made through requests can still be filtered.
with vcr.use_cassette(cass_file, filter_post_data_parameters=['id']) as cass:
assert b'id=secret' not in cass.requests[0].body
'''
url = httpbin_both.url + '/post'
cass_loc = str(tmpdir.join('filter_post_params.yaml'))
with vcr.use_cassette(cass_loc, filter_post_data_parameters=['key']) as cass:
requests.post(url, data={'key': 'value'})
with vcr.use_cassette(cass_loc, filter_post_data_parameters=['key']) as cass:
assert b'key=value' not in cass.requests[0].body

View File

@@ -1,42 +1,133 @@
import vcr
import zlib
import json
import six.moves.http_client as httplib
def _headers_are_case_insensitive():
conn = httplib.HTTPConnection('httpbin.org')
from assertions import assert_is_json
def _headers_are_case_insensitive(host, port):
conn = httplib.HTTPConnection(host, port)
conn.request('GET', "/cookies/set?k1=v1")
r1 = conn.getresponse()
cookie_data1 = r1.getheader('set-cookie')
conn = httplib.HTTPConnection('httpbin.org')
conn = httplib.HTTPConnection(host, port)
conn.request('GET', "/cookies/set?k1=v1")
r2 = conn.getresponse()
cookie_data2 = r2.getheader('Set-Cookie')
return cookie_data1 == cookie_data2
def test_case_insensitivity(tmpdir):
def test_case_insensitivity(tmpdir, httpbin):
testfile = str(tmpdir.join('case_insensitivity.yml'))
# check if headers are case insensitive outside of vcrpy
outside = _headers_are_case_insensitive()
host, port = httpbin.host, httpbin.port
outside = _headers_are_case_insensitive(host, port)
with vcr.use_cassette(testfile):
# check if headers are case insensitive inside of vcrpy
inside = _headers_are_case_insensitive()
inside = _headers_are_case_insensitive(host, port)
# check if headers are case insensitive after vcrpy deserializes headers
inside2 = _headers_are_case_insensitive()
inside2 = _headers_are_case_insensitive(host, port)
# behavior should be the same both inside and outside
assert outside == inside == inside2
def _multiple_header_value(httpserver):
conn = httplib.HTTPConnection('%s:%s' % httpserver.server_address)
conn.request('GET', "/")
def _multiple_header_value(httpbin):
conn = httplib.HTTPConnection(httpbin.host, httpbin.port)
conn.request('GET', "/response-headers?foo=bar&foo=baz")
r = conn.getresponse()
return r.getheader('foo')
def test_multiple_headers(tmpdir, httpserver):
def test_multiple_headers(tmpdir, httpbin):
testfile = str(tmpdir.join('multiple_headers.yaml'))
httpserver.serve_content('Hello!', headers=[('foo', 'bar'), ('foo', 'baz')])
outside = _multiple_header_value(httpserver)
outside = _multiple_header_value(httpbin)
with vcr.use_cassette(testfile):
inside = _multiple_header_value(httpserver)
inside = _multiple_header_value(httpbin)
assert outside == inside
def test_original_decoded_response_is_not_modified(tmpdir, httpbin):
testfile = str(tmpdir.join('decoded_response.yml'))
host, port = httpbin.host, httpbin.port
conn = httplib.HTTPConnection(host, port)
conn.request('GET', '/gzip')
outside = conn.getresponse()
with vcr.use_cassette(testfile, decode_compressed_response=True):
conn = httplib.HTTPConnection(host, port)
conn.request('GET', '/gzip')
inside = conn.getresponse()
# Assert that we do not modify the original response while appending
# to the casssette.
assert 'gzip' == inside.headers['content-encoding']
# They should effectively be the same response.
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].lower() != 'date')
assert set(inside_headers) == set(outside_headers)
inside = zlib.decompress(inside.read(), 16+zlib.MAX_WBITS)
outside = zlib.decompress(outside.read(), 16+zlib.MAX_WBITS)
assert inside == outside
# Even though the above are raw bytes, the JSON data should have been
# decoded and saved to the cassette.
with vcr.use_cassette(testfile):
conn = httplib.HTTPConnection(host, port)
conn.request('GET', '/gzip')
inside = conn.getresponse()
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

View File

@@ -0,0 +1,383 @@
# -*- coding: utf-8 -*-
'''Test requests' interaction with vcr'''
import json
import pytest
import vcr
from vcr.errors import CannotOverwriteExistingCassetteException
from assertions import assert_cassette_empty, assert_is_json
tornado = pytest.importorskip("tornado")
http = pytest.importorskip("tornado.httpclient")
# whether the current version of Tornado supports the raise_error argument for
# fetch().
supports_raise_error = tornado.version_info >= (4,)
@pytest.fixture(params=['simple', 'curl', 'default'])
def get_client(request):
if request.param == 'simple':
from tornado import simple_httpclient as simple
return (lambda: simple.SimpleAsyncHTTPClient())
elif request.param == 'curl':
curl = pytest.importorskip("tornado.curl_httpclient")
return (lambda: curl.CurlAsyncHTTPClient())
else:
return (lambda: http.AsyncHTTPClient())
def get(client, url, **kwargs):
fetch_kwargs = {}
if supports_raise_error:
fetch_kwargs['raise_error'] = kwargs.pop('raise_error', True)
return client.fetch(
http.HTTPRequest(url, method='GET', **kwargs),
**fetch_kwargs
)
def post(client, url, data=None, **kwargs):
if data:
kwargs['body'] = json.dumps(data)
return client.fetch(http.HTTPRequest(url, method='POST', **kwargs))
@pytest.fixture(params=["https", "http"])
def scheme(request):
'''Fixture that returns both http and https.'''
return request.param
@pytest.mark.gen_test
def test_status_code(get_client, scheme, tmpdir):
'''Ensure that we can read the status code'''
url = scheme + '://httpbin.org/'
with vcr.use_cassette(str(tmpdir.join('atts.yaml'))):
status_code = (yield get(get_client(), url)).code
with vcr.use_cassette(str(tmpdir.join('atts.yaml'))) as cass:
assert status_code == (yield get(get_client(), url)).code
assert 1 == cass.play_count
@pytest.mark.gen_test
def test_headers(get_client, scheme, tmpdir):
'''Ensure that we can read the headers back'''
url = scheme + '://httpbin.org/'
with vcr.use_cassette(str(tmpdir.join('headers.yaml'))):
headers = (yield get(get_client(), url)).headers
with vcr.use_cassette(str(tmpdir.join('headers.yaml'))) as cass:
assert headers == (yield get(get_client(), url)).headers
assert 1 == cass.play_count
@pytest.mark.gen_test
def test_body(get_client, tmpdir, scheme):
'''Ensure the responses are all identical enough'''
url = scheme + '://httpbin.org/bytes/1024'
with vcr.use_cassette(str(tmpdir.join('body.yaml'))):
content = (yield get(get_client(), url)).body
with vcr.use_cassette(str(tmpdir.join('body.yaml'))) as cass:
assert content == (yield get(get_client(), url)).body
assert 1 == cass.play_count
@pytest.mark.gen_test
def test_effective_url(get_client, scheme, tmpdir):
'''Ensure that the effective_url is captured'''
url = scheme + '://httpbin.org/redirect-to?url=/html'
with vcr.use_cassette(str(tmpdir.join('url.yaml'))):
effective_url = (yield get(get_client(), url)).effective_url
assert effective_url == scheme + '://httpbin.org/html'
with vcr.use_cassette(str(tmpdir.join('url.yaml'))) as cass:
assert effective_url == (yield get(get_client(), url)).effective_url
assert 1 == cass.play_count
@pytest.mark.gen_test
def test_auth(get_client, tmpdir, scheme):
'''Ensure that we can handle basic auth'''
auth = ('user', 'passwd')
url = scheme + '://httpbin.org/basic-auth/user/passwd'
with vcr.use_cassette(str(tmpdir.join('auth.yaml'))):
one = yield get(
get_client(), url, auth_username=auth[0], auth_password=auth[1]
)
with vcr.use_cassette(str(tmpdir.join('auth.yaml'))) as cass:
two = yield get(
get_client(), url, auth_username=auth[0], auth_password=auth[1]
)
assert one.body == two.body
assert one.code == two.code
assert 1 == cass.play_count
@pytest.mark.gen_test
def test_auth_failed(get_client, tmpdir, scheme):
'''Ensure that we can save failed auth statuses'''
auth = ('user', 'wrongwrongwrong')
url = scheme + '://httpbin.org/basic-auth/user/passwd'
with vcr.use_cassette(str(tmpdir.join('auth-failed.yaml'))) as cass:
# Ensure that this is empty to begin with
assert_cassette_empty(cass)
with pytest.raises(http.HTTPError) as exc_info:
yield get(
get_client(),
url,
auth_username=auth[0],
auth_password=auth[1],
)
one = exc_info.value.response
assert exc_info.value.code == 401
with vcr.use_cassette(str(tmpdir.join('auth-failed.yaml'))) as cass:
with pytest.raises(http.HTTPError) as exc_info:
two = yield get(
get_client(),
url,
auth_username=auth[0],
auth_password=auth[1],
)
two = exc_info.value.response
assert exc_info.value.code == 401
assert one.body == two.body
assert one.code == two.code == 401
assert 1 == cass.play_count
@pytest.mark.gen_test
def test_post(get_client, tmpdir, scheme):
'''Ensure that we can post and cache the results'''
data = {'key1': 'value1', 'key2': 'value2'}
url = scheme + '://httpbin.org/post'
with vcr.use_cassette(str(tmpdir.join('requests.yaml'))):
req1 = (yield post(get_client(), url, data)).body
with vcr.use_cassette(str(tmpdir.join('requests.yaml'))) as cass:
req2 = (yield post(get_client(), url, data)).body
assert req1 == req2
assert 1 == cass.play_count
@pytest.mark.gen_test
def test_redirects(get_client, tmpdir, scheme):
'''Ensure that we can handle redirects'''
url = scheme + '://httpbin.org/redirect-to?url=bytes/1024'
with vcr.use_cassette(str(tmpdir.join('requests.yaml'))):
content = (yield get(get_client(), url)).body
with vcr.use_cassette(str(tmpdir.join('requests.yaml'))) as cass:
assert content == (yield get(get_client(), url)).body
assert cass.play_count == 1
@pytest.mark.gen_test
def test_cross_scheme(get_client, tmpdir, scheme):
'''Ensure that requests between schemes are treated separately'''
# First fetch a url under http, and then again under https and then
# ensure that we haven't served anything out of cache, and we have two
# requests / response pairs in the cassette
with vcr.use_cassette(str(tmpdir.join('cross_scheme.yaml'))) as cass:
yield get(get_client(), 'https://httpbin.org/')
yield get(get_client(), 'http://httpbin.org/')
assert cass.play_count == 0
assert len(cass) == 2
# Then repeat the same requests and ensure both were replayed.
with vcr.use_cassette(str(tmpdir.join('cross_scheme.yaml'))) as cass:
yield get(get_client(), 'https://httpbin.org/')
yield get(get_client(), 'http://httpbin.org/')
assert cass.play_count == 2
@pytest.mark.gen_test
def test_gzip(get_client, tmpdir, scheme):
'''
Ensure that httpclient is able to automatically decompress the response
body
'''
url = scheme + '://httpbin.org/gzip'
# use_gzip was renamed to decompress_response in 4.0
kwargs = {}
if tornado.version_info < (4,):
kwargs['use_gzip'] = True
else:
kwargs['decompress_response'] = True
with vcr.use_cassette(str(tmpdir.join('gzip.yaml'))):
response = yield get(get_client(), url, **kwargs)
assert_is_json(response.body)
with vcr.use_cassette(str(tmpdir.join('gzip.yaml'))) as cass:
response = yield get(get_client(), url, **kwargs)
assert_is_json(response.body)
assert 1 == cass.play_count
@pytest.mark.gen_test
def test_https_with_cert_validation_disabled(get_client, tmpdir):
cass_path = str(tmpdir.join('cert_validation_disabled.yaml'))
with vcr.use_cassette(cass_path):
yield get(get_client(), 'https://httpbin.org', validate_cert=False)
with vcr.use_cassette(cass_path) as cass:
yield get(get_client(), 'https://httpbin.org', validate_cert=False)
assert 1 == cass.play_count
@pytest.mark.gen_test
def test_unsupported_features_raises_in_future(get_client, tmpdir):
'''Ensure that the exception for an AsyncHTTPClient feature not being
supported is raised inside the future.'''
def callback(chunk):
assert False, "Did not expect to be called."
with vcr.use_cassette(str(tmpdir.join('invalid.yaml'))):
future = get(
get_client(), 'http://httpbin.org', streaming_callback=callback
)
with pytest.raises(Exception) as excinfo:
yield future
assert "not yet supported by VCR" in str(excinfo)
@pytest.mark.skipif(
not supports_raise_error,
reason='raise_error unavailable in tornado <= 3',
)
@pytest.mark.gen_test
def test_unsupported_features_raise_error_disabled(get_client, tmpdir):
'''Ensure that the exception for an AsyncHTTPClient feature not being
supported is not raised if raise_error=False.'''
def callback(chunk):
assert False, "Did not expect to be called."
with vcr.use_cassette(str(tmpdir.join('invalid.yaml'))):
response = yield get(
get_client(),
'http://httpbin.org',
streaming_callback=callback,
raise_error=False,
)
assert "not yet supported by VCR" in str(response.error)
@pytest.mark.gen_test
def test_cannot_overwrite_cassette_raises_in_future(get_client, tmpdir):
'''Ensure that CannotOverwriteExistingCassetteException is raised inside
the future.'''
with vcr.use_cassette(str(tmpdir.join('overwrite.yaml'))):
yield get(get_client(), 'http://httpbin.org/get')
with vcr.use_cassette(str(tmpdir.join('overwrite.yaml'))):
future = get(get_client(), 'http://httpbin.org/headers')
with pytest.raises(CannotOverwriteExistingCassetteException):
yield future
@pytest.mark.skipif(
not supports_raise_error,
reason='raise_error unavailable in tornado <= 3',
)
@pytest.mark.gen_test
def test_cannot_overwrite_cassette_raise_error_disabled(get_client, tmpdir):
'''Ensure that CannotOverwriteExistingCassetteException is not raised if
raise_error=False in the fetch() call.'''
with vcr.use_cassette(str(tmpdir.join('overwrite.yaml'))):
yield get(
get_client(), 'http://httpbin.org/get', raise_error=False
)
with vcr.use_cassette(str(tmpdir.join('overwrite.yaml'))):
response = yield get(
get_client(), 'http://httpbin.org/headers', raise_error=False
)
assert isinstance(response.error, CannotOverwriteExistingCassetteException)
@pytest.mark.gen_test
@vcr.use_cassette(path_transformer=vcr.default_vcr.ensure_suffix('.yaml'))
def test_tornado_with_decorator_use_cassette(get_client):
response = yield get_client().fetch(
http.HTTPRequest('http://www.google.com/', method='GET')
)
assert response.body.decode('utf-8') == "not actually google"
@pytest.mark.gen_test
@vcr.use_cassette(path_transformer=vcr.default_vcr.ensure_suffix('.yaml'))
def test_tornado_exception_can_be_caught(get_client):
try:
yield get(get_client(), 'http://httpbin.org/status/500')
except http.HTTPError as e:
assert e.code == 500
try:
yield get(get_client(), 'http://httpbin.org/status/404')
except http.HTTPError as e:
assert e.code == 404
@pytest.mark.gen_test
def test_existing_references_get_patched(tmpdir):
from tornado.httpclient import AsyncHTTPClient
with vcr.use_cassette(str(tmpdir.join('data.yaml'))):
client = AsyncHTTPClient()
yield get(client, 'http://httpbin.org/get')
with vcr.use_cassette(str(tmpdir.join('data.yaml'))) as cass:
yield get(client, 'http://httpbin.org/get')
assert cass.play_count == 1
@pytest.mark.gen_test
def test_existing_instances_get_patched(get_client, tmpdir):
'''Ensure that existing instances of AsyncHTTPClient get patched upon
entering VCR context.'''
client = get_client()
with vcr.use_cassette(str(tmpdir.join('data.yaml'))):
yield get(client, 'http://httpbin.org/get')
with vcr.use_cassette(str(tmpdir.join('data.yaml'))) as cass:
yield get(client, 'http://httpbin.org/get')
assert cass.play_count == 1
@pytest.mark.gen_test
def test_request_time_is_set(get_client, tmpdir):
'''Ensures that the request_time on HTTPResponses is set.'''
with vcr.use_cassette(str(tmpdir.join('data.yaml'))):
client = get_client()
response = yield get(client, 'http://httpbin.org/get')
assert response.request_time is not None
with vcr.use_cassette(str(tmpdir.join('data.yaml'))) as cass:
client = get_client()
response = yield get(client, 'http://httpbin.org/get')
assert response.request_time is not None
assert cass.play_count == 1

View File

@@ -0,0 +1,62 @@
interactions:
- request:
body: null
headers: {}
method: GET
uri: http://httpbin.org/status/500
response:
body: {string: !!python/unicode ''}
headers:
- !!python/tuple
- Content-Length
- ['0']
- !!python/tuple
- Server
- [nginx]
- !!python/tuple
- Connection
- [close]
- !!python/tuple
- Access-Control-Allow-Credentials
- ['true']
- !!python/tuple
- Date
- ['Thu, 30 Jul 2015 17:32:39 GMT']
- !!python/tuple
- Access-Control-Allow-Origin
- ['*']
- !!python/tuple
- Content-Type
- [text/html; charset=utf-8]
status: {code: 500, message: INTERNAL SERVER ERROR}
- request:
body: null
headers: {}
method: GET
uri: http://httpbin.org/status/404
response:
body: {string: !!python/unicode ''}
headers:
- !!python/tuple
- Content-Length
- ['0']
- !!python/tuple
- Server
- [nginx]
- !!python/tuple
- Connection
- [close]
- !!python/tuple
- Access-Control-Allow-Credentials
- ['true']
- !!python/tuple
- Date
- ['Thu, 30 Jul 2015 17:32:39 GMT']
- !!python/tuple
- Access-Control-Allow-Origin
- ['*']
- !!python/tuple
- Content-Type
- [text/html; charset=utf-8]
status: {code: 404, message: NOT FOUND}
version: 1

View File

@@ -0,0 +1,53 @@
interactions:
- request:
body: null
headers: {}
method: GET
uri: http://www.google.com/
response:
body: {string: !!python/unicode 'not actually google'}
headers:
- !!python/tuple
- Expires
- ['-1']
- !!python/tuple
- Connection
- [close]
- !!python/tuple
- P3p
- ['CP="This is not a P3P policy! See http://www.google.com/support/accounts/bin/answer.py?hl=en&answer=151657
for more info."']
- !!python/tuple
- Alternate-Protocol
- ['80:quic,p=0']
- !!python/tuple
- Accept-Ranges
- [none]
- !!python/tuple
- X-Xss-Protection
- [1; mode=block]
- !!python/tuple
- Vary
- [Accept-Encoding]
- !!python/tuple
- Date
- ['Thu, 30 Jul 2015 08:41:40 GMT']
- !!python/tuple
- Cache-Control
- ['private, max-age=0']
- !!python/tuple
- Content-Type
- [text/html; charset=ISO-8859-1]
- !!python/tuple
- Set-Cookie
- ['PREF=ID=1111111111111111:FF=0:TM=1438245700:LM=1438245700:V=1:S=GAzVO0ALebSpC_cJ;
expires=Sat, 29-Jul-2017 08:41:40 GMT; path=/; domain=.google.com', 'NID=69=Br7oRAwgmKoK__HC6FEnuxglTFDmFxqP6Md63lKhzW1w6WkDbp3U90CDxnUKvDP6wJH8yxY5Lk5ZnFf66Q1B0d4OsYoKgq0vjfBAYXuCIAWtOuGZEOsFXanXs7pt2Mjx;
expires=Fri, 29-Jan-2016 08:41:40 GMT; path=/; domain=.google.com; HttpOnly']
- !!python/tuple
- X-Frame-Options
- [SAMEORIGIN]
- !!python/tuple
- Server
- [gws]
status: {code: 200, message: OK}
version: 1

View File

@@ -1,131 +1,149 @@
# -*- coding: utf-8 -*-
'''Integration tests with urllib2'''
# coding=utf-8
# External imports
import os
import pytest
import ssl
from six.moves.urllib.request import urlopen
from six.moves.urllib_parse import urlencode
import pytest_httpbin.certs
# Internal imports
import vcr
from assertions import assert_cassette_empty, assert_cassette_has_one_response
from assertions import assert_cassette_has_one_response
@pytest.fixture(params=["https", "http"])
def scheme(request):
"""
Fixture that returns both http and https
"""
return request.param
def urlopen_with_cafile(*args, **kwargs):
context = ssl.create_default_context(cafile=pytest_httpbin.certs.where())
context.check_hostname = False
kwargs['context'] = context
try:
return urlopen(*args, **kwargs)
except TypeError:
# python2/pypi don't let us override this
del kwargs['cafile']
return urlopen(*args, **kwargs)
def test_response_code(scheme, tmpdir):
def test_response_code(httpbin_both, tmpdir):
'''Ensure we can read a response code from a fetch'''
url = scheme + '://httpbin.org/'
with vcr.use_cassette(str(tmpdir.join('atts.yaml'))) as cass:
code = urlopen(url).getcode()
url = httpbin_both.url
with vcr.use_cassette(str(tmpdir.join('atts.yaml'))):
code = urlopen_with_cafile(url).getcode()
with vcr.use_cassette(str(tmpdir.join('atts.yaml'))) as cass:
assert code == urlopen(url).getcode()
with vcr.use_cassette(str(tmpdir.join('atts.yaml'))):
assert code == urlopen_with_cafile(url).getcode()
def test_random_body(scheme, tmpdir):
def test_random_body(httpbin_both, tmpdir):
'''Ensure we can read the content, and that it's served from cache'''
url = scheme + '://httpbin.org/bytes/1024'
with vcr.use_cassette(str(tmpdir.join('body.yaml'))) as cass:
body = urlopen(url).read()
url = httpbin_both.url + '/bytes/1024'
with vcr.use_cassette(str(tmpdir.join('body.yaml'))):
body = urlopen_with_cafile(url).read()
with vcr.use_cassette(str(tmpdir.join('body.yaml'))) as cass:
assert body == urlopen(url).read()
with vcr.use_cassette(str(tmpdir.join('body.yaml'))):
assert body == urlopen_with_cafile(url).read()
def test_response_headers(scheme, tmpdir):
def test_response_headers(httpbin_both, tmpdir):
'''Ensure we can get information from the response'''
url = scheme + '://httpbin.org/'
with vcr.use_cassette(str(tmpdir.join('headers.yaml'))) as cass:
open1 = urlopen(url).info().items()
url = httpbin_both.url
with vcr.use_cassette(str(tmpdir.join('headers.yaml'))):
open1 = urlopen_with_cafile(url).info().items()
with vcr.use_cassette(str(tmpdir.join('headers.yaml'))):
open2 = urlopen_with_cafile(url).info().items()
with vcr.use_cassette(str(tmpdir.join('headers.yaml'))) as cass:
open2 = urlopen(url).info().items()
assert sorted(open1) == sorted(open2)
def test_multiple_requests(scheme, tmpdir):
def test_effective_url(httpbin_both, tmpdir):
'''Ensure that the effective_url is captured'''
url = httpbin_both.url + '/redirect-to?url=/html'
with vcr.use_cassette(str(tmpdir.join('headers.yaml'))):
effective_url = urlopen_with_cafile(url).geturl()
assert effective_url == httpbin_both.url + '/html'
with vcr.use_cassette(str(tmpdir.join('headers.yaml'))):
assert effective_url == urlopen_with_cafile(url).geturl()
def test_multiple_requests(httpbin_both, tmpdir):
'''Ensure that we can cache multiple requests'''
urls = [
scheme + '://httpbin.org/',
scheme + '://httpbin.org/',
scheme + '://httpbin.org/get',
scheme + '://httpbin.org/bytes/1024'
httpbin_both.url,
httpbin_both.url,
httpbin_both.url + '/get',
httpbin_both.url + '/bytes/1024',
]
with vcr.use_cassette(str(tmpdir.join('multiple.yaml'))) as cass:
[urlopen(url) for url in urls]
[urlopen_with_cafile(url) for url in urls]
assert len(cass) == len(urls)
def test_get_data(scheme, tmpdir):
def test_get_data(httpbin_both, tmpdir):
'''Ensure that it works with query data'''
data = urlencode({'some': 1, 'data': 'here'})
url = scheme + '://httpbin.org/get?' + data
with vcr.use_cassette(str(tmpdir.join('get_data.yaml'))) as cass:
res1 = urlopen(url).read()
with vcr.use_cassette(str(tmpdir.join('get_data.yaml'))) as cass:
res2 = urlopen(url).read()
url = httpbin_both.url + '/get?' + data
with vcr.use_cassette(str(tmpdir.join('get_data.yaml'))):
res1 = urlopen_with_cafile(url).read()
with vcr.use_cassette(str(tmpdir.join('get_data.yaml'))):
res2 = urlopen_with_cafile(url).read()
assert res1 == res2
def test_post_data(scheme, tmpdir):
def test_post_data(httpbin_both, tmpdir):
'''Ensure that it works when posting data'''
data = urlencode({'some': 1, 'data': 'here'}).encode('utf-8')
url = scheme + '://httpbin.org/post'
with vcr.use_cassette(str(tmpdir.join('post_data.yaml'))) as cass:
res1 = urlopen(url, data).read()
url = httpbin_both.url + '/post'
with vcr.use_cassette(str(tmpdir.join('post_data.yaml'))):
res1 = urlopen_with_cafile(url, data).read()
with vcr.use_cassette(str(tmpdir.join('post_data.yaml'))) as cass:
res2 = urlopen(url, data).read()
res2 = urlopen_with_cafile(url, data).read()
assert len(cass) == 1
assert res1 == res2
assert_cassette_has_one_response(cass)
def test_post_unicode_data(scheme, tmpdir):
def test_post_unicode_data(httpbin_both, tmpdir):
'''Ensure that it works when posting unicode data'''
data = urlencode({'snowman': u''.encode('utf-8')}).encode('utf-8')
url = scheme + '://httpbin.org/post'
url = httpbin_both.url + '/post'
with vcr.use_cassette(str(tmpdir.join('post_data.yaml'))):
res1 = urlopen_with_cafile(url, data).read()
with vcr.use_cassette(str(tmpdir.join('post_data.yaml'))) as cass:
res1 = urlopen(url, data).read()
with vcr.use_cassette(str(tmpdir.join('post_data.yaml'))) as cass:
res2 = urlopen(url, data).read()
res2 = urlopen_with_cafile(url, data).read()
assert len(cass) == 1
assert res1 == res2
assert_cassette_has_one_response(cass)
def test_cross_scheme(tmpdir):
def test_cross_scheme(tmpdir, httpbin_secure, httpbin):
'''Ensure that requests between schemes are treated separately'''
# First fetch a url under https, and then again under https and then
# ensure that we haven't served anything out of cache, and we have two
# requests / response pairs in the cassette
with vcr.use_cassette(str(tmpdir.join('cross_scheme.yaml'))) as cass:
urlopen('https://httpbin.org/')
urlopen('http://httpbin.org/')
urlopen_with_cafile(httpbin_secure.url)
urlopen_with_cafile(httpbin.url)
assert len(cass) == 2
assert cass.play_count == 0
def test_decorator(scheme, tmpdir):
def test_decorator(httpbin_both, tmpdir):
'''Test the decorator version of VCR.py'''
url = scheme + '://httpbin.org/'
url = httpbin_both.url
@vcr.use_cassette(str(tmpdir.join('atts.yaml')))
def inner1():
return urlopen(url).getcode()
return urlopen_with_cafile(url).getcode()
@vcr.use_cassette(str(tmpdir.join('atts.yaml')))
def inner2():
return urlopen(url).getcode()
return urlopen_with_cafile(url).getcode()
assert inner1() == inner2()

View File

@@ -0,0 +1,159 @@
'''Integration tests with urllib3'''
# coding=utf-8
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")
@pytest.fixture(scope='module')
def verify_pool_mgr():
return urllib3.PoolManager(
cert_reqs='CERT_REQUIRED', # Force certificate check.
ca_certs=pytest_httpbin.certs.where()
)
@pytest.fixture(scope='module')
def pool_mgr():
return urllib3.PoolManager()
def test_status_code(httpbin_both, tmpdir, verify_pool_mgr):
'''Ensure that we can read the status code'''
url = httpbin_both.url
with vcr.use_cassette(str(tmpdir.join('atts.yaml'))):
status_code = verify_pool_mgr.request('GET', url).status
with vcr.use_cassette(str(tmpdir.join('atts.yaml'))):
assert status_code == verify_pool_mgr.request('GET', url).status
def test_headers(tmpdir, httpbin_both, verify_pool_mgr):
'''Ensure that we can read the headers back'''
url = httpbin_both.url
with vcr.use_cassette(str(tmpdir.join('headers.yaml'))):
headers = verify_pool_mgr.request('GET', url).headers
with vcr.use_cassette(str(tmpdir.join('headers.yaml'))):
assert headers == verify_pool_mgr.request('GET', url).headers
def test_body(tmpdir, httpbin_both, verify_pool_mgr):
'''Ensure the responses are all identical enough'''
url = httpbin_both.url + '/bytes/1024'
with vcr.use_cassette(str(tmpdir.join('body.yaml'))):
content = verify_pool_mgr.request('GET', url).data
with vcr.use_cassette(str(tmpdir.join('body.yaml'))):
assert content == verify_pool_mgr.request('GET', url).data
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='{}:{}'.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)
with vcr.use_cassette(str(tmpdir.join('auth.yaml'))):
two = verify_pool_mgr.request('GET', url, headers=headers)
assert one.data == two.data
assert one.status == two.status
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='{}:{}'.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
assert_cassette_empty(cass)
one = verify_pool_mgr.request('GET', url, headers=headers)
two = verify_pool_mgr.request('GET', url, headers=headers)
assert one.data == two.data
assert one.status == two.status == 401
def test_post(tmpdir, httpbin_both, verify_pool_mgr):
'''Ensure that we can post and cache the results'''
data = {'key1': 'value1', 'key2': 'value2'}
url = httpbin_both.url + '/post'
with vcr.use_cassette(str(tmpdir.join('verify_pool_mgr.yaml'))):
req1 = verify_pool_mgr.request('POST', url, data).data
with vcr.use_cassette(str(tmpdir.join('verify_pool_mgr.yaml'))):
req2 = verify_pool_mgr.request('POST', url, data).data
assert req1 == req2
def test_redirects(tmpdir, httpbin_both, verify_pool_mgr):
'''Ensure that we can handle redirects'''
url = httpbin_both.url + '/redirect-to?url=bytes/1024'
with vcr.use_cassette(str(tmpdir.join('verify_pool_mgr.yaml'))):
content = verify_pool_mgr.request('GET', url).data
with vcr.use_cassette(str(tmpdir.join('verify_pool_mgr.yaml'))) as cass:
assert content == verify_pool_mgr.request('GET', url).data
# Ensure that we've now cached *two* responses. One for the redirect
# and one for the final fetch
assert len(cass) == 2
assert cass.play_count == 2
def test_cross_scheme(tmpdir, httpbin, httpbin_secure, verify_pool_mgr):
'''Ensure that requests between schemes are treated separately'''
# First fetch a url under http, and then again under https and then
# ensure that we haven't served anything out of cache, and we have two
# requests / response pairs in the cassette
with vcr.use_cassette(str(tmpdir.join('cross_scheme.yaml'))) as cass:
verify_pool_mgr.request('GET', httpbin_secure.url)
verify_pool_mgr.request('GET', httpbin.url)
assert cass.play_count == 0
assert len(cass) == 2
def test_gzip(tmpdir, httpbin_both, verify_pool_mgr):
'''
Ensure that requests (actually urllib3) is able to automatically decompress
the response body
'''
url = httpbin_both.url + '/gzip'
response = verify_pool_mgr.request('GET', url)
with vcr.use_cassette(str(tmpdir.join('gzip.yaml'))):
response = verify_pool_mgr.request('GET', url)
assert_is_json(response.data)
with vcr.use_cassette(str(tmpdir.join('gzip.yaml'))):
assert_is_json(response.data)
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

View File

@@ -1,7 +1,10 @@
import multiprocessing
import pytest
from six.moves import xmlrpc_client, xmlrpc_server
requests = pytest.importorskip("requests")
import vcr
import vcr # NOQA
try:
import httplib
@@ -22,14 +25,14 @@ def test_domain_redirect():
assert len(cass) == 2
def test_flickr_multipart_upload():
def test_flickr_multipart_upload(httpbin, tmpdir):
"""
The python-flickr-api project does a multipart
upload that confuses vcrpy
"""
def _pretend_to_be_flickr_library():
content_type, body = "text/plain", "HELLO WORLD"
h = httplib.HTTPConnection("httpbin.org")
h = httplib.HTTPConnection(httpbin.host, httpbin.port)
headers = {
"Content-Type": content_type,
"content-length": str(len(body))
@@ -40,11 +43,14 @@ def test_flickr_multipart_upload():
data = r.read()
h.close()
with vcr.use_cassette('fixtures/vcr_cassettes/flickr.yaml') as cass:
return data
testfile = str(tmpdir.join('flickr.yml'))
with vcr.use_cassette(testfile) as cass:
_pretend_to_be_flickr_library()
assert len(cass) == 1
with vcr.use_cassette('fixtures/vcr_cassettes/flickr.yaml') as cass:
with vcr.use_cassette(testfile) as cass:
assert len(cass) == 1
_pretend_to_be_flickr_library()
assert cass.play_count == 1
@@ -57,10 +63,45 @@ def test_flickr_should_respond_with_200(tmpdir):
assert r.status_code == 200
def test_cookies(tmpdir):
def test_cookies(tmpdir, httpbin):
testfile = str(tmpdir.join('cookies.yml'))
with vcr.use_cassette(testfile):
s = requests.Session()
r1 = s.get("http://httpbin.org/cookies/set?k1=v1&k2=v2")
r2 = s.get("http://httpbin.org/cookies")
s.get(httpbin.url + "/cookies/set?k1=v1&k2=v2")
r2 = s.get(httpbin.url + "/cookies")
assert len(r2.json()['cookies']) == 2
def test_amazon_doctype(tmpdir):
# amazon gzips its homepage. For some reason, in requests 2.7, it's not
# getting gunzipped.
with vcr.use_cassette(str(tmpdir.join('amz.yml'))):
r = requests.get('http://www.amazon.com', verify=False)
assert 'html' in r.text
@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(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(rpc_server, allow_none=True)
second_schema = roundup_server.pow(2, 4)
assert original_schema == second_schema

View File

@@ -1,14 +1,16 @@
import copy
import inspect
import os
from six.moves import http_client as httplib
import contextlib2
import mock
import pytest
import yaml
from vcr.compat import mock, contextlib
from vcr.cassette import Cassette
from vcr.patch import force_reset
from vcr.errors import UnhandledHTTPRequestError
from vcr.patch import force_reset
from vcr.stubs import VCRHTTPSConnection
def test_cassette_load(tmpdir):
@@ -17,7 +19,7 @@ def test_cassette_load(tmpdir):
{'request': {'body': '', 'uri': 'foo', 'method': 'GET', 'headers': {}},
'response': 'bar'}
]}))
a_cassette = Cassette.load(str(a_file))
a_cassette = Cassette.load(path=str(a_file))
assert len(a_cassette) == 1
@@ -81,37 +83,40 @@ 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):
decorated_function = Cassette.use('test')(make_get_request)
for i in range(2):
decorated_function()
decorated_function = Cassette.use(path='test')(make_get_request)
for i in range(4):
decorated_function()
def test_arg_getter_functionality():
arg_getter = mock.Mock(return_value=('test', {}))
arg_getter = mock.Mock(return_value={'path': 'test'})
context_decorator = Cassette.use_arg_getter(arg_getter)
with context_decorator as cassette:
assert cassette._path == 'test'
arg_getter.return_value = ('other', {})
arg_getter.return_value = {'path': 'other'}
with context_decorator as cassette:
assert cassette._path == 'other'
arg_getter.return_value = ('', {'filter_headers': ('header_name',)})
arg_getter.return_value = {'path': 'other', 'filter_headers': ('header_name',)}
@context_decorator
def function():
pass
with mock.patch.object(Cassette, 'load') as cassette_load:
with mock.patch.object(
Cassette, 'load',
return_value=mock.MagicMock(inject=False)
) as cassette_load:
function()
cassette_load.assert_called_once_with(arg_getter.return_value[0],
**arg_getter.return_value[1])
cassette_load.assert_called_once_with(**arg_getter.return_value)
def test_cassette_not_all_played():
@@ -153,14 +158,14 @@ def test_nesting_cassette_context_managers(*args):
second_response = copy.deepcopy(first_response)
second_response['body']['string'] = b'second_response'
with contextlib2.ExitStack() as exit_stack:
first_cassette = exit_stack.enter_context(Cassette.use('test'))
with contextlib.ExitStack() as exit_stack:
first_cassette = exit_stack.enter_context(Cassette.use(path='test'))
exit_stack.enter_context(mock.patch.object(first_cassette, 'play_response',
return_value=first_response))
return_value=first_response))
assert_get_response_body_is('first_response')
# Make sure a second cassette can supercede the first
with Cassette.use('test') as second_cassette:
with Cassette.use(path='test') as second_cassette:
with mock.patch.object(second_cassette, 'play_response', return_value=second_response):
assert_get_response_body_is('second_response')
@@ -170,14 +175,134 @@ def test_nesting_cassette_context_managers(*args):
def test_nesting_context_managers_by_checking_references_of_http_connection():
original = httplib.HTTPConnection
with Cassette.use('test'):
with Cassette.use(path='test'):
first_cassette_HTTPConnection = httplib.HTTPConnection
with Cassette.use('test'):
with Cassette.use(path='test'):
second_cassette_HTTPConnection = httplib.HTTPConnection
assert second_cassette_HTTPConnection is not first_cassette_HTTPConnection
with Cassette.use('test'):
with Cassette.use(path='test'):
assert httplib.HTTPConnection is not second_cassette_HTTPConnection
with force_reset():
assert httplib.HTTPConnection is original
assert httplib.HTTPConnection is second_cassette_HTTPConnection
assert httplib.HTTPConnection is first_cassette_HTTPConnection
def test_custom_patchers():
class Test(object):
attribute = None
with Cassette.use(path='custom_patches',
custom_patches=((Test, 'attribute', VCRHTTPSConnection),)):
assert issubclass(Test.attribute, VCRHTTPSConnection)
assert VCRHTTPSConnection is not Test.attribute
old_attribute = Test.attribute
with Cassette.use(path='custom_patches',
custom_patches=((Test, 'attribute', VCRHTTPSConnection),)):
assert issubclass(Test.attribute, VCRHTTPSConnection)
assert VCRHTTPSConnection is not Test.attribute
assert Test.attribute is not old_attribute
assert issubclass(Test.attribute, VCRHTTPSConnection)
assert VCRHTTPSConnection is not Test.attribute
assert Test.attribute is old_attribute
def test_decorated_functions_are_reentrant():
info = {"second": False}
original_conn = httplib.HTTPConnection
@Cassette.use(path='whatever', inject=True)
def test_function(cassette):
if info['second']:
assert httplib.HTTPConnection is not info['first_conn']
else:
info['first_conn'] = httplib.HTTPConnection
info['second'] = True
test_function()
assert httplib.HTTPConnection is info['first_conn']
test_function()
assert httplib.HTTPConnection is original_conn
def test_cassette_use_called_without_path_uses_function_to_generate_path():
@Cassette.use(inject=True)
def function_name(cassette):
assert cassette._path == 'function_name'
function_name()
def test_path_transformer_with_function_path():
def path_transformer(path):
return os.path.join('a', path)
@Cassette.use(inject=True, path_transformer=path_transformer)
def function_name(cassette):
assert cassette._path == os.path.join('a', 'function_name')
function_name()
def test_path_transformer_with_context_manager():
with Cassette.use(
path='b', path_transformer=lambda *args: 'a'
) as cassette:
assert cassette._path == 'a'
def test_path_transformer_None():
with Cassette.use(
path='a', path_transformer=None,
) as cassette:
assert cassette._path == 'a'
def test_func_path_generator():
def generator(function):
return os.path.join(os.path.dirname(inspect.getfile(function)),
function.__name__)
@Cassette.use(inject=True, func_path_generator=generator)
def function_name(cassette):
assert cassette._path == os.path.join(os.path.dirname(__file__), 'function_name')
function_name()
def test_use_as_decorator_on_coroutine():
original_http_connetion = httplib.HTTPConnection
@Cassette.use(inject=True)
def test_function(cassette):
assert httplib.HTTPConnection.cassette is cassette
assert httplib.HTTPConnection is not original_http_connetion
value = yield 1
assert value == 1
assert httplib.HTTPConnection.cassette is cassette
assert httplib.HTTPConnection is not original_http_connetion
value = yield 2
assert value == 2
coroutine = test_function()
value = next(coroutine)
while True:
try:
value = coroutine.send(value)
except StopIteration:
break
def test_use_as_decorator_on_generator():
original_http_connetion = httplib.HTTPConnection
@Cassette.use(inject=True)
def test_function(cassette):
assert httplib.HTTPConnection.cassette is cassette
assert httplib.HTTPConnection is not original_http_connetion
yield 1
assert httplib.HTTPConnection.cassette is cassette
assert httplib.HTTPConnection is not original_http_connetion
yield 2
assert list(test_function()) == [1, 2]

View File

@@ -1,37 +1,274 @@
from vcr.filters import remove_headers, remove_query_parameters
from six import BytesIO
from vcr.filters import (
remove_headers, replace_headers,
remove_query_parameters, replace_query_parameters,
remove_post_data_parameters, replace_post_data_parameters,
decode_response
)
from vcr.compat import mock
from vcr.request import Request
import gzip
import json
import zlib
def test_replace_headers():
# This tests all of:
# 1. keeping a header
# 2. removing a header
# 3. replacing a header
# 4. replacing a header using a callable
# 5. removing a header using a callable
# 6. replacing a header that doesn't exist
headers = {
'one': ['keep'],
'two': ['lose'],
'three': ['change'],
'four': ['shout'],
'five': ['whisper'],
}
request = Request('GET', 'http://google.com', '', headers)
replace_headers(request, [
('two', None),
('three', 'tada'),
('four', lambda key, value, request: value.upper()),
('five', lambda key, value, request: None),
('six', 'doesntexist'),
])
assert request.headers == {
'one': 'keep',
'three': 'tada',
'four': 'SHOUT',
}
def test_replace_headers_empty():
headers = {'hello': 'goodbye', 'secret': 'header'}
request = Request('GET', 'http://google.com', '', headers)
replace_headers(request, [])
assert request.headers == headers
def test_replace_headers_callable():
# This goes beyond test_replace_headers() to ensure that the callable
# receives the expected arguments.
headers = {'hey': 'there'}
request = Request('GET', 'http://google.com', '', headers)
callme = mock.Mock(return_value='ho')
replace_headers(request, [('hey', callme)])
assert request.headers == {'hey': 'ho'}
assert callme.call_args == ((), {'request': request,
'key': 'hey',
'value': 'there'})
def test_remove_headers():
# Test the backward-compatible API wrapper.
headers = {'hello': ['goodbye'], 'secret': ['header']}
request = Request('GET', 'http://google.com', '', headers)
remove_headers(request, ['secret'])
assert request.headers == {'hello': 'goodbye'}
def test_remove_headers_empty():
headers = {'hello': 'goodbye', 'secret': 'header'}
request = Request('GET', 'http://google.com', '', headers)
remove_headers(request, [])
assert request.headers == headers
def test_replace_query_parameters():
# This tests all of:
# 1. keeping a parameter
# 2. removing a parameter
# 3. replacing a parameter
# 4. replacing a parameter using a callable
# 5. removing a parameter using a callable
# 6. replacing a parameter that doesn't exist
uri = 'http://g.com/?one=keep&two=lose&three=change&four=shout&five=whisper'
request = Request('GET', uri, '', {})
replace_query_parameters(request, [
('two', None),
('three', 'tada'),
('four', lambda key, value, request: value.upper()),
('five', lambda key, value, request: None),
('six', 'doesntexist'),
])
assert request.query == [
('four', 'SHOUT'),
('one', 'keep'),
('three', 'tada'),
]
def test_remove_all_query_parameters():
uri = 'http://g.com/?q=cowboys&w=1'
request = Request('GET', uri, '', {})
replace_query_parameters(request, [('w', None), ('q', None)])
assert request.uri == 'http://g.com/'
def test_replace_query_parameters_callable():
# This goes beyond test_replace_query_parameters() to ensure that the
# callable receives the expected arguments.
uri = 'http://g.com/?hey=there'
request = Request('GET', uri, '', {})
callme = mock.Mock(return_value='ho')
replace_query_parameters(request, [('hey', callme)])
assert request.uri == 'http://g.com/?hey=ho'
assert callme.call_args == ((), {'request': request,
'key': 'hey',
'value': 'there'})
def test_remove_query_parameters():
# Test the backward-compatible API wrapper.
uri = 'http://g.com/?q=cowboys&w=1'
request = Request('GET', uri, '', {})
remove_query_parameters(request, ['w'])
assert request.uri == 'http://g.com/?q=cowboys'
def test_remove_all_query_parameters():
uri = 'http://g.com/?q=cowboys&w=1'
request = Request('GET', uri, '', {})
remove_query_parameters(request, ['w', 'q'])
assert request.uri == 'http://g.com/'
def test_replace_post_data_parameters():
# This tests all of:
# 1. keeping a parameter
# 2. removing a parameter
# 3. replacing a parameter
# 4. replacing a parameter using a callable
# 5. removing a parameter using a callable
# 6. replacing a parameter that doesn't exist
body = b'one=keep&two=lose&three=change&four=shout&five=whisper'
request = Request('POST', 'http://google.com', body, {})
replace_post_data_parameters(request, [
('two', None),
('three', 'tada'),
('four', lambda key, value, request: value.upper()),
('five', lambda key, value, request: None),
('six', 'doesntexist'),
])
assert request.body == b'one=keep&three=tada&four=SHOUT'
def test_remove_nonexistent_query_parameters():
uri = 'http://g.com/'
request = Request('GET', uri, '', {})
remove_query_parameters(request, ['w', 'q'])
assert request.uri == 'http://g.com/'
def test_remove_post_data_parameters():
# Test the backward-compatible API wrapper.
body = b'id=secret&foo=bar'
request = Request('POST', 'http://google.com', body, {})
remove_post_data_parameters(request, ['id'])
assert request.body == b'foo=bar'
def test_preserve_multiple_post_data_parameters():
body = b'id=secret&foo=bar&foo=baz'
request = Request('POST', 'http://google.com', body, {})
replace_post_data_parameters(request, [('id', None)])
assert request.body == b'foo=bar&foo=baz'
def test_remove_all_post_data_parameters():
body = b'id=secret&foo=bar'
request = Request('POST', 'http://google.com', body, {})
replace_post_data_parameters(request, [('id', None), ('foo', None)])
assert request.body == b''
def test_replace_json_post_data_parameters():
# This tests all of:
# 1. keeping a parameter
# 2. removing a parameter
# 3. replacing a parameter
# 4. replacing a parameter using a callable
# 5. removing a parameter using a callable
# 6. replacing a parameter that doesn't exist
body = b'{"one": "keep", "two": "lose", "three": "change", "four": "shout", "five": "whisper"}'
request = Request('POST', 'http://google.com', body, {})
request.headers['Content-Type'] = 'application/json'
replace_post_data_parameters(request, [
('two', None),
('three', 'tada'),
('four', lambda key, value, request: value.upper()),
('five', lambda key, value, request: None),
('six', 'doesntexist'),
])
request_data = json.loads(request.body.decode('utf-8'))
expected_data = json.loads('{"one": "keep", "three": "tada", "four": "SHOUT"}')
assert request_data == expected_data
def test_remove_json_post_data_parameters():
# Test the backward-compatible API wrapper.
body = b'{"id": "secret", "foo": "bar", "baz": "qux"}'
request = Request('POST', 'http://google.com', body, {})
request.headers['Content-Type'] = 'application/json'
remove_post_data_parameters(request, ['id'])
request_body_json = json.loads(request.body.decode('utf-8'))
expected_json = json.loads(b'{"foo": "bar", "baz": "qux"}'.decode('utf-8'))
assert request_body_json == expected_json
def test_remove_all_json_post_data_parameters():
body = b'{"id": "secret", "foo": "bar"}'
request = Request('POST', 'http://google.com', body, {})
request.headers['Content-Type'] = 'application/json'
replace_post_data_parameters(request, [('id', None), ('foo', None)])
assert request.body == b'{}'
def test_decode_response_uncompressed():
recorded_response = {
"status": {
"message": "OK",
"code": 200
},
"headers": {
"content-length": ["10806"],
"date": ["Fri, 24 Oct 2014 18:35:37 GMT"],
"content-type": ["text/html; charset=utf-8"],
},
"body": {
"string": b""
}
}
assert decode_response(recorded_response) == recorded_response
def test_decode_response_deflate():
body = b'deflate message'
deflate_response = {
'body': {'string': zlib.compress(body)},
'headers': {
'access-control-allow-credentials': ['true'],
'access-control-allow-origin': ['*'],
'connection': ['keep-alive'],
'content-encoding': ['deflate'],
'content-length': ['177'],
'content-type': ['application/json'],
'date': ['Wed, 02 Dec 2015 19:44:32 GMT'],
'server': ['nginx']
},
'status': {'code': 200, 'message': 'OK'}
}
decoded_response = decode_response(deflate_response)
assert decoded_response['body']['string'] == body
assert decoded_response['headers']['content-length'] == [str(len(body))]
def test_decode_response_gzip():
body = b'gzip message'
buf = BytesIO()
f = gzip.GzipFile('a', fileobj=buf, mode='wb')
f.write(body)
f.close()
compressed_body = buf.getvalue()
buf.close()
gzip_response = {
'body': {'string': compressed_body},
'headers': {
'access-control-allow-credentials': ['true'],
'access-control-allow-origin': ['*'],
'connection': ['keep-alive'],
'content-encoding': ['gzip'],
'content-length': ['177'],
'content-type': ['application/json'],
'date': ['Wed, 02 Dec 2015 19:44:32 GMT'],
'server': ['nginx']
},
'status': {'code': 200, 'message': 'OK'}
}
decoded_response = decode_response(gzip_response)
assert decoded_response['body']['string'] == body
assert decoded_response['headers']['content-length'] == [str(len(body))]

View File

@@ -1,5 +1,7 @@
import itertools
import pytest
from vcr import matchers
from vcr import request
@@ -20,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
@@ -29,12 +31,113 @@ 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
req1_body = (b"<?xml version='1.0'?><methodCall><methodName>test</methodName>"
b"<params><param><value><array><data><value><struct>"
b"<member><name>a</name><value><string>1</string></value></member>"
b"<member><name>b</name><value><string>2</string></value></member>"
b"</struct></value></data></array></value></param></params></methodCall>")
req2_body = (b"<?xml version='1.0'?><methodCall><methodName>test</methodName>"
b"<params><param><value><array><data><value><struct>"
b"<member><name>b</name><value><string>2</string></value></member>"
b"<member><name>a</name><value><string>1</string></value></member>"
b"</struct></value></data></array></value></param></params></methodCall>")
@pytest.mark.parametrize("r1, r2", [
(
request.Request('POST', 'http://host.com/', '123', {}),
request.Request('POST', 'http://another-host.com/',
'123', {'Some-Header': 'value'})
),
(
request.Request('POST', 'http://host.com/', 'a=1&b=2',
{'Content-Type': 'application/x-www-form-urlencoded'}),
request.Request('POST', 'http://host.com/', 'b=2&a=1',
{'Content-Type': 'application/x-www-form-urlencoded'})
),
(
request.Request('POST', 'http://host.com/', '123', {}),
request.Request('POST', 'http://another-host.com/', '123', {'Some-Header': 'value'})
),
(
request.Request(
'POST', 'http://host.com/', 'a=1&b=2',
{'Content-Type': 'application/x-www-form-urlencoded'}
),
request.Request(
'POST', 'http://host.com/', 'b=2&a=1',
{'Content-Type': 'application/x-www-form-urlencoded'}
)
),
(
request.Request(
'POST', 'http://host.com/', '{"a": 1, "b": 2}',
{'Content-Type': 'application/json'}
),
request.Request(
'POST', 'http://host.com/', '{"b": 2, "a": 1}',
{'content-type': 'application/json'}
)
),
(
request.Request(
'POST', 'http://host.com/', req1_body,
{'User-Agent': 'xmlrpclib', 'Content-Type': 'text/xml'}
),
request.Request(
'POST', 'http://host.com/', req2_body,
{'user-agent': 'somexmlrpc', 'content-type': 'text/xml'}
)
),
(
request.Request(
'POST', 'http://host.com/',
'{"a": 1, "b": 2}', {'Content-Type': 'application/json'}
),
request.Request(
'POST', 'http://host.com/',
'{"b": 2, "a": 1}', {'content-type': 'application/json'}
)
)
])
def test_body_matcher_does_match(r1, r2):
assert matchers.body(r1, r2)
@pytest.mark.parametrize("r1, r2", [
(
request.Request('POST', 'http://host.com/', '{"a": 1, "b": 2}', {}),
request.Request('POST', 'http://host.com/', '{"b": 2, "a": 1}', {}),
),
(
request.Request(
'POST', 'http://host.com/',
'{"a": 1, "b": 3}', {'Content-Type': 'application/json'}
),
request.Request(
'POST', 'http://host.com/',
'{"b": 2, "a": 1}', {'content-type': 'application/json'}
)
),
(
request.Request(
'POST', 'http://host.com/', req1_body, {'Content-Type': 'text/xml'}
),
request.Request(
'POST', 'http://host.com/', req2_body, {'content-type': 'text/xml'}
)
)
])
def test_body_match_does_not_match(r1, r2):
assert not matchers.body(r1, r2)
def test_query_matcher():
req1 = request.Request('GET', 'http://host.com/?a=b&c=d', '', {})
req2 = request.Request('GET', 'http://host.com/?c=d&a=b', '', {})

View File

@@ -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()

View File

@@ -1,6 +1,6 @@
import pytest
from vcr.request import Request
from vcr.request import Request, HeadersDict
def test_str():
@@ -12,21 +12,26 @@ def test_headers():
headers = {'X-Header1': ['h1'], 'X-Header2': 'h2'}
req = Request('GET', 'http://go.com/', '', headers)
assert req.headers == {'X-Header1': 'h1', 'X-Header2': 'h2'}
req.add_header('X-Header1', 'h11')
req.headers['X-Header1'] = 'h11'
assert req.headers == {'X-Header1': 'h11', 'X-Header2': 'h2'}
def test_add_header_deprecated():
req = Request('GET', 'http://go.com/', '', {})
pytest.deprecated_call(req.add_header, 'foo', 'bar')
assert req.headers == {'foo': 'bar'}
@pytest.mark.parametrize("uri, expected_port", [
('http://go.com/', 80),
('http://go.com:80/', 80),
('http://go.com:3000/', 3000),
('https://go.com/', 433),
('https://go.com:433/', 433),
('https://go.com/', 443),
('https://go.com:443/', 443),
('https://go.com:3000/', 3000),
])
])
def test_port(uri, expected_port):
req = Request('GET', uri, '', {})
req = Request('GET', uri, '', {})
assert req.port == expected_port
@@ -36,3 +41,30 @@ def test_uri():
req = Request('GET', 'http://go.com:80/', '', {})
assert req.uri == 'http://go.com:80/'
def test_HeadersDict():
# Simple test of CaseInsensitiveDict
h = HeadersDict()
assert h == {}
h['Content-Type'] = 'application/json'
assert h == {'Content-Type': 'application/json'}
assert h['content-type'] == 'application/json'
assert h['CONTENT-TYPE'] == 'application/json'
# Test feature of HeadersDict: devolve list to first element
h = HeadersDict()
assert h == {}
h['x'] = ['foo', 'bar']
assert h == {'x': 'foo'}
# Test feature of HeadersDict: preserve original key case
h = HeadersDict()
assert h == {}
h['Content-Type'] = 'application/json'
assert h == {'Content-Type': 'application/json'}
h['content-type'] = 'text/plain'
assert h == {'Content-Type': 'text/plain'}
h['CONtent-tyPE'] = 'whoa'
assert h == {'Content-Type': 'whoa'}

View File

@@ -0,0 +1,68 @@
# coding: UTF-8
from vcr.stubs import VCRHTTPResponse
def test_response_should_have_headers_field():
recorded_response = {
"status": {
"message": "OK",
"code": 200
},
"headers": {
"content-length": ["0"],
"server": ["gunicorn/18.0"],
"connection": ["Close"],
"access-control-allow-credentials": ["true"],
"date": ["Fri, 24 Oct 2014 18:35:37 GMT"],
"access-control-allow-origin": ["*"],
"content-type": ["text/html; charset=utf-8"],
},
"body": {
"string": b""
}
}
response = VCRHTTPResponse(recorded_response)
assert response.headers is not None
def test_response_headers_should_be_equal_to_msg():
recorded_response = {
"status": {
"message": b"OK",
"code": 200
},
"headers": {
"content-length": ["0"],
"server": ["gunicorn/18.0"],
"connection": ["Close"],
"content-type": ["text/html; charset=utf-8"],
},
"body": {
"string": b""
}
}
response = VCRHTTPResponse(recorded_response)
assert response.headers == response.msg
def test_response_headers_should_have_correct_values():
recorded_response = {
"status": {
"message": "OK",
"code": 200
},
"headers": {
"content-length": ["10806"],
"date": ["Fri, 24 Oct 2014 18:35:37 GMT"],
"content-type": ["text/html; charset=utf-8"],
},
"body": {
"string": b""
}
}
response = VCRHTTPResponse(recorded_response)
assert response.headers.get('content-length') == "10806"
assert response.headers.get('date') == "Fri, 24 Oct 2014 18:35:37 GMT"

View File

@@ -1,8 +1,10 @@
import mock
# -*- encoding: utf-8 -*-
import pytest
from vcr.serialize import deserialize
from vcr.serializers import yamlserializer, jsonserializer
from vcr.compat import mock
from vcr.request import Request
from vcr.serialize import deserialize, serialize
from vcr.serializers import yamlserializer, jsonserializer, compat
def test_deserialize_old_yaml_cassette():
@@ -27,9 +29,111 @@ def test_deserialize_new_json_cassette():
deserialize(f.read(), jsonserializer)
REQBODY_TEMPLATE = u'''\
interactions:
- request:
body: {req_body}
headers:
Content-Type: [application/x-www-form-urlencoded]
Host: [httpbin.org]
method: POST
uri: http://httpbin.org/post
response:
body: {{string: ""}}
headers:
content-length: ['0']
content-type: [application/json]
status: {{code: 200, message: OK}}
'''
# A cassette generated under Python 2 stores the request body as a string,
# but the same cassette generated under Python 3 stores it as "!!binary".
# Make sure we accept both forms, regardless of whether we're running under
# Python 2 or 3.
@pytest.mark.parametrize("req_body, expect", [
# Cassette written under Python 2 (pure ASCII body)
('x=5&y=2', b'x=5&y=2'),
# Cassette written under Python 3 (pure ASCII body)
('!!binary |\n eD01Jnk9Mg==', b'x=5&y=2'),
# Request body has non-ASCII chars (x=föo&y=2), encoded in UTF-8.
('!!python/str "x=f\\xF6o&y=2"', b'x=f\xc3\xb6o&y=2'),
('!!binary |\n eD1mw7ZvJnk9Mg==', b'x=f\xc3\xb6o&y=2'),
# Same request body, this time encoded in UTF-16. In this case, we
# write the same YAML file under both Python 2 and 3, so there's only
# one test case here.
('!!binary |\n //54AD0AZgD2AG8AJgB5AD0AMgA=',
b'\xff\xfex\x00=\x00f\x00\xf6\x00o\x00&\x00y\x00=\x002\x00'),
# Same again, this time encoded in ISO-8859-1.
('!!binary |\n eD1m9m8meT0y', b'x=f\xf6o&y=2'),
])
def test_deserialize_py2py3_yaml_cassette(tmpdir, req_body, expect):
cfile = tmpdir.join('test_cassette.yaml')
cfile.write(REQBODY_TEMPLATE.format(req_body=req_body))
with open(str(cfile)) as f:
(requests, responses) = deserialize(f.read(), yamlserializer)
assert requests[0].body == expect
@mock.patch.object(jsonserializer.json, 'dumps',
side_effect=UnicodeDecodeError('utf-8', b'unicode error in serialization',
0, 10, 'blew up'))
def test_serialize_constructs_UnicodeDecodeError(mock_dumps):
with pytest.raises(UnicodeDecodeError):
jsonserializer.serialize({})
def test_serialize_empty_request():
request = Request(
method='POST',
uri='http://localhost/',
body='',
headers={},
)
serialize(
{'requests': [request], 'responses': [{}]},
jsonserializer
)
def test_serialize_json_request():
request = Request(
method='POST',
uri='http://localhost/',
body="{'hello': 'world'}",
headers={},
)
serialize(
{'requests': [request], 'responses': [{}]},
jsonserializer
)
def test_serialize_binary_request():
msg = "Does this HTTP interaction contain binary data?"
request = Request(
method='POST',
uri='http://localhost/',
body=b'\x8c',
headers={},
)
try:
serialize(
{'requests': [request], 'responses': [{}]},
jsonserializer
)
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

18
tests/unit/test_stubs.py Normal file
View File

@@ -0,0 +1,18 @@
from vcr.stubs import VCRHTTPSConnection
from vcr.compat import mock
from vcr.cassette import Cassette
class TestVCRConnection(object):
def test_setting_of_attributes_get_propogated_to_real_connection(self):
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

View File

@@ -1,17 +1,27 @@
import mock
import os
import pytest
from six.moves import http_client as httplib
from vcr import VCR, use_cassette
from vcr.compat import mock
from vcr.request import Request
from vcr.stubs import VCRHTTPSConnection
from vcr.patch import _HTTPConnection, force_reset
def test_vcr_use_cassette():
record_mode = mock.Mock()
test_vcr = VCR(record_mode=record_mode)
with mock.patch('vcr.cassette.Cassette.load') as mock_cassette_load:
with mock.patch(
'vcr.cassette.Cassette.load',
return_value=mock.MagicMock(inject=False)
) as mock_cassette_load:
@test_vcr.use_cassette('test')
def function():
pass
assert mock_cassette_load.call_count == 0
function()
assert mock_cassette_load.call_args[1]['record_mode'] is record_mode
@@ -32,30 +42,112 @@ def test_vcr_use_cassette():
def test_vcr_before_record_request_params():
base_path = 'http://httpbin.org/'
def before_record_cb(request):
if request.path != '/get':
return request
test_vcr = VCR(filter_headers=('cookie',), before_record_request=before_record_cb,
test_vcr = VCR(filter_headers=('cookie', ('bert', 'ernie')),
before_record_request=before_record_cb,
ignore_hosts=('www.test.com',), ignore_localhost=True,
filter_query_parameters=('foo',))
filter_query_parameters=('foo', ('tom', 'jerry')),
filter_post_data_parameters=('posted', ('no', 'trespassing')))
with test_vcr.use_cassette('test') as cassette:
assert cassette.filter_request(Request('GET', base_path + 'get', '', {})) is None
assert cassette.filter_request(Request('GET', base_path + 'get2', '', {})) is not None
# Test explicit before_record_cb
request_get = Request('GET', base_path + 'get', '', {})
assert cassette.filter_request(request_get) is None
request = Request('GET', base_path + 'get2', '', {})
assert cassette.filter_request(request) is not None
assert cassette.filter_request(Request('GET', base_path + '?foo=bar', '', {})).query == []
assert cassette.filter_request(
Request('GET', base_path + '?foo=bar', '',
{'cookie': 'test', 'other': 'fun'})).headers == {'other': 'fun'}
assert cassette.filter_request(Request('GET', base_path + '?foo=bar', '',
{'cookie': 'test', 'other': 'fun'})).headers == {'other': 'fun'}
# Test filter_query_parameters
request = Request('GET', base_path + '?foo=bar', '', {})
assert cassette.filter_request(request).query == []
request = Request('GET', base_path + '?tom=nobody', '', {})
assert cassette.filter_request(request).query == [('tom', 'jerry')]
assert cassette.filter_request(Request('GET', 'http://www.test.com' + '?foo=bar', '',
{'cookie': 'test', 'other': 'fun'})) is None
# Test filter_headers
request = Request('GET', base_path + '?foo=bar', '',
{'cookie': 'test', 'other': 'fun', 'bert': 'nobody'})
assert (cassette.filter_request(request).headers ==
{'other': 'fun', 'bert': 'ernie'})
# Test ignore_hosts
request = Request('GET', 'http://www.test.com' + '?foo=bar', '',
{'cookie': 'test', 'other': 'fun'})
assert cassette.filter_request(request) is None
# Test ignore_localhost
request = Request('GET', 'http://localhost:8000' + '?foo=bar', '',
{'cookie': 'test', 'other': 'fun'})
assert cassette.filter_request(request) is None
with test_vcr.use_cassette('test', before_record_request=None) as cassette:
# Test that before_record can be overwritten with
assert cassette.filter_request(Request('GET', base_path + 'get', '', {})) is not None
# Test that before_record can be overwritten in context manager.
assert cassette.filter_request(request_get) is not None
def test_vcr_before_record_response_iterable():
# Regression test for #191
request = Request('GET', '/', '', {})
response = object() # just can't be None
# Prevent actually saving the cassette
with mock.patch('vcr.cassette.FilesystemPersister.save_cassette'):
# Baseline: non-iterable before_record_response should work
mock_filter = mock.Mock()
vcr = VCR(before_record_response=mock_filter)
with vcr.use_cassette('test') as cassette:
assert mock_filter.call_count == 0
cassette.append(request, response)
assert mock_filter.call_count == 1
# Regression test: iterable before_record_response should work too
mock_filter = mock.Mock()
vcr = VCR(before_record_response=(mock_filter,))
with vcr.use_cassette('test') as cassette:
assert mock_filter.call_count == 0
cassette.append(request, response)
assert mock_filter.call_count == 1
def test_before_record_response_as_filter():
request = Request('GET', '/', '', {})
response = object() # just can't be None
# Prevent actually saving the cassette
with mock.patch('vcr.cassette.FilesystemPersister.save_cassette'):
filter_all = mock.Mock(return_value=None)
vcr = VCR(before_record_response=filter_all)
with vcr.use_cassette('test') as cassette:
cassette.append(request, response)
assert cassette.data == []
assert not cassette.dirty
def test_vcr_path_transformer():
# Regression test for #199
# Prevent actually saving the cassette
with mock.patch('vcr.cassette.FilesystemPersister.save_cassette'):
# Baseline: path should be unchanged
vcr = VCR()
with vcr.use_cassette('test') as cassette:
assert cassette._path == 'test'
# Regression test: path_transformer=None should do the same.
vcr = VCR(path_transformer=None)
with vcr.use_cassette('test') as cassette:
assert cassette._path == 'test'
# and it should still work with cassette_library_dir
vcr = VCR(cassette_library_dir='/foo')
with vcr.use_cassette('test') as cassette:
assert cassette._path == '/foo/test'
@pytest.fixture
@@ -69,8 +161,206 @@ def test_fixtures_with_use_cassette(random_fixture):
# problems if the decorator does not preserve the signature of the original
# test function.
# This test ensures that use_cassette preserves the signature of the original
# test function, and thus that use_cassette is compatible with py.test
# fixtures. It is admittedly a bit strange because the test would never even
# run if the relevant feature were broken.
# This test ensures that use_cassette preserves the signature of
# the original test function, and thus that use_cassette is
# compatible with py.test fixtures. It is admittedly a bit strange
# because the test would never even run if the relevant feature
# were broken.
pass
def test_custom_patchers():
class Test(object):
attribute = None
attribute2 = None
test_vcr = VCR(custom_patches=((Test, 'attribute', VCRHTTPSConnection),))
with test_vcr.use_cassette('custom_patches'):
assert issubclass(Test.attribute, VCRHTTPSConnection)
assert VCRHTTPSConnection is not Test.attribute
with test_vcr.use_cassette(
'custom_patches',
custom_patches=((Test, 'attribute2', VCRHTTPSConnection),)
):
assert issubclass(Test.attribute, VCRHTTPSConnection)
assert VCRHTTPSConnection is not Test.attribute
assert Test.attribute is Test.attribute2
def test_inject_cassette():
vcr = VCR(inject_cassette=True)
@vcr.use_cassette('test', record_mode='once')
def with_cassette_injected(cassette):
assert cassette.record_mode == 'once'
@vcr.use_cassette('test', record_mode='once', inject_cassette=False)
def without_cassette_injected():
pass
with_cassette_injected()
without_cassette_injected()
def test_with_current_defaults():
vcr = VCR(inject_cassette=True, record_mode='once')
@vcr.use_cassette('test', with_current_defaults=False)
def changing_defaults(cassette, checks):
checks(cassette)
@vcr.use_cassette('test', with_current_defaults=True)
def current_defaults(cassette, checks):
checks(cassette)
def assert_record_mode_once(cassette):
assert cassette.record_mode == 'once'
def assert_record_mode_all(cassette):
assert cassette.record_mode == 'all'
changing_defaults(assert_record_mode_once)
current_defaults(assert_record_mode_once)
vcr.record_mode = 'all'
changing_defaults(assert_record_mode_all)
current_defaults(assert_record_mode_once)
def test_cassette_library_dir_with_decoration_and_no_explicit_path():
library_dir = '/libary_dir'
vcr = VCR(inject_cassette=True, cassette_library_dir=library_dir)
@vcr.use_cassette()
def function_name(cassette):
assert cassette._path == os.path.join(library_dir, 'function_name')
function_name()
def test_cassette_library_dir_with_decoration_and_explicit_path():
library_dir = '/libary_dir'
vcr = VCR(inject_cassette=True, cassette_library_dir=library_dir)
@vcr.use_cassette(path='custom_name')
def function_name(cassette):
assert cassette._path == os.path.join(library_dir, 'custom_name')
function_name()
def test_cassette_library_dir_with_decoration_and_super_explicit_path():
library_dir = '/libary_dir'
vcr = VCR(inject_cassette=True, cassette_library_dir=library_dir)
@vcr.use_cassette(path=os.path.join(library_dir, 'custom_name'))
def function_name(cassette):
assert cassette._path == os.path.join(library_dir, 'custom_name')
function_name()
def test_cassette_library_dir_with_path_transformer():
library_dir = '/libary_dir'
vcr = VCR(inject_cassette=True, cassette_library_dir=library_dir,
path_transformer=lambda path: path + '.json')
@vcr.use_cassette()
def function_name(cassette):
assert cassette._path == os.path.join(library_dir, 'function_name.json')
function_name()
def test_use_cassette_with_no_extra_invocation():
vcr = VCR(inject_cassette=True, cassette_library_dir='/')
@vcr.use_cassette
def function_name(cassette):
assert cassette._path == os.path.join('/', 'function_name')
function_name()
def test_path_transformer():
vcr = VCR(inject_cassette=True, cassette_library_dir='/',
path_transformer=lambda x: x + '_test')
@vcr.use_cassette
def function_name(cassette):
assert cassette._path == os.path.join('/', 'function_name_test')
function_name()
def test_cassette_name_generator_defaults_to_using_module_function_defined_in():
vcr = VCR(inject_cassette=True)
@vcr.use_cassette
def function_name(cassette):
assert cassette._path == os.path.join(os.path.dirname(__file__),
'function_name')
function_name()
def test_ensure_suffix():
vcr = VCR(inject_cassette=True, path_transformer=VCR.ensure_suffix('.yaml'))
@vcr.use_cassette
def function_name(cassette):
assert cassette._path == os.path.join(os.path.dirname(__file__),
'function_name.yaml')
function_name()
def test_additional_matchers():
vcr = VCR(match_on=('uri',), inject_cassette=True)
@vcr.use_cassette
def function_defaults(cassette):
assert set(cassette._match_on) == {vcr.matchers['uri']}
@vcr.use_cassette(additional_matchers=('body',))
def function_additional(cassette):
assert set(cassette._match_on) == {vcr.matchers['uri'], vcr.matchers['body']}
function_defaults()
function_additional()
def test_decoration_should_respect_function_return_value():
vcr = VCR()
ret = 'a-return-value'
@vcr.use_cassette
def function_with_return():
return ret
assert ret == function_with_return()
class TestVCRClass(VCR().test_case()):
def no_decoration(self):
assert httplib.HTTPConnection == _HTTPConnection
self.test_dynamically_added()
assert httplib.HTTPConnection == _HTTPConnection
def test_one(self):
with force_reset():
self.no_decoration()
with force_reset():
self.test_two()
assert httplib.HTTPConnection != _HTTPConnection
def test_two(self):
assert httplib.HTTPConnection != _HTTPConnection
def test_dynamically_added(self):
assert httplib.HTTPConnection != _HTTPConnection
TestVCRClass.test_dynamically_added = test_dynamically_added
del test_dynamically_added

39
tox.ini
View File

@@ -1,24 +1,33 @@
[tox]
envlist = {py26,py27,py33,py34,pypy}-{requests24,requests23,requests22,requests1,httplib2,urllib3,boto}
envlist = {py27,py35,py36,py37,pypy}-{flakes,requests27,httplib2,urllib3121,tornado4,boto3,aiohttp}
[testenv:flakes]
skipsdist = True
commands =
flake8 --version
flake8 --exclude=./docs/conf.py,./.tox/
pyflakes ./docs/conf.py
deps = flake8
[testenv]
commands =
py.test {posargs}
basepython =
py26: python2.6
py27: python2.7
py33: python3.3
py34: python3.4
pypy: pypy
./runtests.sh {posargs}
deps =
Flask<1
mock
pytest
pytest-localserver
pytest-httpbin
PyYAML
requests1: requests==1.2.3
requests24: requests==2.4.0
requests23: requests==2.3.0
requests22: requests==2.2.1
requests27: requests==2.7.0
httplib2: httplib2
urllib3: urllib3==1.7.1
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
View File

@@ -0,0 +1,3 @@
async def handle_coroutine(vcr, fn): # noqa: E999
with vcr as cassette:
return (await fn(cassette)) # noqa: E999

View File

@@ -1,19 +1,28 @@
"""The container for recorded requests and responses"""
import collections
import copy
import sys
import inspect
import logging
import contextlib2
import wrapt
try:
from collections import Counter
except ImportError:
from .compat.counter import Counter
# Internal imports
from .patch import CassettePatcherBuilder
from .persist import load_cassette, save_cassette
from .serializers import yamlserializer
from .matchers import requests_match, uri, method
from .compat import contextlib
from .errors import UnhandledHTTPRequestError
from .matchers import requests_match, uri, method
from .patch import CassettePatcherBuilder
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__)
@@ -23,15 +32,25 @@ class CassetteContextDecorator(object):
"""Context manager/decorator that handles installing the cassette and
removing cassettes.
This class defers the creation of a new cassette instance until the point at
which it is installed by context manager or decorator. The fact that a new
cassette is used with each application prevents the state of any cassette
from interfering with another.
This class defers the creation of a new cassette instance until
the point at which it is installed by context manager or
decorator. The fact that a new cassette is used with each
application prevents the state of any cassette from interfering
with another.
Instances of this class are NOT reentrant as context managers.
However, functions that are decorated by
``CassetteContextDecorator`` instances ARE reentrant. See the
implementation of ``__call__`` on this class for more details.
There is also a guard against attempts to reenter instances of
this class as a context manager in ``__exit__``.
"""
_non_cassette_arguments = ('path_transformer', 'func_path_generator')
@classmethod
def from_args(cls, cassette_class, path, **kwargs):
return cls(cassette_class, lambda: (path, kwargs))
def from_args(cls, cassette_class, **kwargs):
return cls(cassette_class, lambda: dict(kwargs))
def __init__(self, cls, args_getter):
self.cls = cls
@@ -39,20 +58,39 @@ class CassetteContextDecorator(object):
self.__finish = None
def _patch_generator(self, cassette):
with contextlib2.ExitStack() as exit_stack:
with contextlib.ExitStack() as exit_stack:
for patcher in CassettePatcherBuilder(cassette).build():
exit_stack.enter_context(patcher)
log.debug('Entered context for cassette at {0}.'.format(cassette._path))
log_format = '{action} context for cassette at {path}.'
log.debug(log_format.format(
action="Entering", path=cassette._path
))
yield cassette
log.debug('Exiting context for cassette at {0}.'.format(cassette._path))
log.debug(log_format.format(
action="Exiting", path=cassette._path
))
# TODO(@IvanMalison): Hmmm. it kind of feels like this should be
# somewhere else.
cassette._save()
def __enter__(self):
# This assertion is here to prevent the dangerous behavior
# that would result from forgetting about a __finish before
# completing it.
# How might this condition be met? Here is an example:
# context_decorator = Cassette.use('whatever')
# with context_decorator:
# with context_decorator:
# pass
assert self.__finish is None, "Cassette already open."
path, kwargs = self._args_getter()
self.__finish = self._patch_generator(self.cls.load(path, **kwargs))
other_kwargs, cassette_kwargs = partition_dict(
lambda key, _: key in self._non_cassette_arguments,
self._args_getter()
)
if other_kwargs.get('path_transformer'):
transformer = other_kwargs['path_transformer']
cassette_kwargs['path'] = transformer(cassette_kwargs['path'])
self.__finish = self._patch_generator(self.cls.load(**cassette_kwargs))
return next(self.__finish)
def __exit__(self, *args):
@@ -61,17 +99,75 @@ class CassetteContextDecorator(object):
@wrapt.decorator
def __call__(self, function, instance, args, kwargs):
with self:
return function(*args, **kwargs)
# This awkward cloning thing is done to ensure that decorated
# functions are reentrant. This is required for thread
# safety and the correct operation of recursive functions.
args_getter = self._build_args_getter_for_decorator(function)
return type(self)(self.cls, args_getter)._execute_function(
function, args, kwargs
)
def _execute_function(self, function, args, kwargs):
def handle_function(cassette):
if cassette.inject:
return function(cassette, *args, **kwargs)
else:
return function(*args, **kwargs)
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 = fn(cassette)
# We don't need to catch StopIteration. The caller (Tornado's
# gen.coroutine, for example) will handle that.
to_yield = next(coroutine)
while True:
try:
to_send = yield to_yield
except Exception:
to_yield = coroutine.throw(*sys.exc_info())
else:
try:
to_yield = coroutine.send(to_send)
except StopIteration:
break
def _handle_function(self, fn):
with self as cassette:
return fn(cassette)
@staticmethod
def get_function_name(function):
return function.__name__
def _build_args_getter_for_decorator(self, function):
def new_args_getter():
kwargs = self._args_getter()
if 'path' not in kwargs:
name_generator = (kwargs.get('func_path_generator') or
self.get_function_name)
path = name_generator(function)
kwargs['path'] = path
return kwargs
return new_args_getter
class Cassette(object):
"""A container for recorded requests and responses"""
@classmethod
def load(cls, path, **kwargs):
def load(cls, **kwargs):
"""Instantiate and load the cassette stored at the specified path."""
new_cassette = cls(path, **kwargs)
new_cassette = cls(**kwargs)
new_cassette._load()
return new_cassette
@@ -80,26 +176,28 @@ class Cassette(object):
return CassetteContextDecorator(cls, arg_getter)
@classmethod
def use(cls, *args, **kwargs):
return CassetteContextDecorator.from_args(cls, *args, **kwargs)
def use(cls, **kwargs):
return CassetteContextDecorator.from_args(cls, **kwargs)
def __init__(self, path, serializer=yamlserializer, record_mode='once',
match_on=(uri, method), filter_headers=(),
filter_query_parameters=(), before_record_request=None,
before_record_response=None, ignore_hosts=(),
ignore_localhost=()):
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)
self.inject = inject
self.record_mode = record_mode
self.custom_patches = custom_patches
# self.data is the list of (req, resp) tuples
self.data = []
self.play_counts = Counter()
self.play_counts = collections.Counter()
self.dirty = False
self.rewound = False
self.record_mode = record_mode
@property
def play_count(self):
@@ -128,8 +226,12 @@ class Cassette(object):
request = self._before_record_request(request)
if not request:
return
if self._before_record_response:
response = self._before_record_response(response)
# 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
self.data.append((request, response))
self.dirty = True
@@ -188,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)
)
@@ -219,6 +321,7 @@ class Cassette(object):
def __contains__(self, request):
"""Return whether or not a request has been stored"""
for response in self._responses(request):
return True
for index, response in self._responses(request):
if self.play_counts[index] == 0:
return True
return False

14
vcr/compat.py Normal file
View File

@@ -0,0 +1,14 @@
try:
from unittest import mock
except ImportError:
import mock
try:
import contextlib
except ImportError:
import contextlib2 as contextlib
else:
if not hasattr(contextlib, 'ExitStack'):
import contextlib2 as contextlib
__all__ = ['mock', 'contextlib']

View File

View File

@@ -1,194 +0,0 @@
from __future__ import print_function
from operator import itemgetter
from heapq import nlargest
from itertools import repeat, ifilter
# From http://code.activestate.com/recipes/576611-counter-class/
# Backported for python 2.6 support
class Counter(dict):
'''Dict subclass for counting hashable objects. Sometimes called a bag
or multiset. Elements are stored as dictionary keys and their counts
are stored as dictionary values.
>>> Counter('zyzygy')
Counter({'y': 3, 'z': 2, 'g': 1})
'''
def __init__(self, iterable=None, **kwds):
'''Create a new, empty Counter object. And if given, count elements
from an input iterable. Or, initialize the count from another mapping
of elements to their counts.
>>> c = Counter() # a new, empty counter
>>> c = Counter('gallahad') # a new counter from an iterable
>>> c = Counter({'a': 4, 'b': 2}) # a new counter from a mapping
>>> c = Counter(a=4, b=2) # a new counter from keyword args
'''
self.update(iterable, **kwds)
def __missing__(self, key):
return 0
def most_common(self, n=None):
'''List the n most common elements and their counts from the most
common to the least. If n is None, then list all element counts.
>>> Counter('abracadabra').most_common(3)
[('a', 5), ('r', 2), ('b', 2)]
'''
if n is None:
return sorted(self.iteritems(), key=itemgetter(1), reverse=True)
return nlargest(n, self.iteritems(), key=itemgetter(1))
def elements(self):
'''Iterator over elements repeating each as many times as its count.
>>> c = Counter('ABCABC')
>>> sorted(c.elements())
['A', 'A', 'B', 'B', 'C', 'C']
If an element's count has been set to zero or is a negative number,
elements() will ignore it.
'''
for elem, count in self.iteritems():
for _ in repeat(None, count):
yield elem
# Override dict methods where the meaning changes for Counter objects.
@classmethod
def fromkeys(cls, iterable, v=None):
raise NotImplementedError(
'Counter.fromkeys() is undefined. Use Counter(iterable) instead.')
def update(self, iterable=None, **kwds):
'''Like dict.update() but add counts instead of replacing them.
Source can be an iterable, a dictionary, or another Counter instance.
>>> c = Counter('which')
>>> c.update('witch') # add elements from another iterable
>>> d = Counter('watch')
>>> c.update(d) # add elements from another counter
>>> c['h'] # four 'h' in which, witch, and watch
4
'''
if iterable is not None:
if hasattr(iterable, 'iteritems'):
if self:
self_get = self.get
for elem, count in iterable.iteritems():
self[elem] = self_get(elem, 0) + count
else:
dict.update(self, iterable) # fast path when counter is empty
else:
self_get = self.get
for elem in iterable:
self[elem] = self_get(elem, 0) + 1
if kwds:
self.update(kwds)
def copy(self):
'Like dict.copy() but returns a Counter instance instead of a dict.'
return Counter(self)
def __delitem__(self, elem):
'Like dict.__delitem__() but does not raise KeyError for missing values.'
if elem in self:
dict.__delitem__(self, elem)
def __repr__(self):
if not self:
return '%s()' % self.__class__.__name__
items = ', '.join(map('%r: %r'.__mod__, self.most_common()))
return '%s({%s})' % (self.__class__.__name__, items)
# Multiset-style mathematical operations discussed in:
# Knuth TAOCP Volume II section 4.6.3 exercise 19
# and at http://en.wikipedia.org/wiki/Multiset
#
# Outputs guaranteed to only include positive counts.
#
# To strip negative and zero counts, add-in an empty counter:
# c += Counter()
def __add__(self, other):
'''Add counts from two counters.
>>> Counter('abbb') + Counter('bcc')
Counter({'b': 4, 'c': 2, 'a': 1})
'''
if not isinstance(other, Counter):
return NotImplemented
result = Counter()
for elem in set(self) | set(other):
newcount = self[elem] + other[elem]
if newcount > 0:
result[elem] = newcount
return result
def __sub__(self, other):
''' Subtract count, but keep only results with positive counts.
>>> Counter('abbbc') - Counter('bccd')
Counter({'b': 2, 'a': 1})
'''
if not isinstance(other, Counter):
return NotImplemented
result = Counter()
for elem in set(self) | set(other):
newcount = self[elem] - other[elem]
if newcount > 0:
result[elem] = newcount
return result
def __or__(self, other):
'''Union is the maximum of value in either of the input counters.
>>> Counter('abbb') | Counter('bcc')
Counter({'b': 3, 'c': 2, 'a': 1})
'''
if not isinstance(other, Counter):
return NotImplemented
_max = max
result = Counter()
for elem in set(self) | set(other):
newcount = _max(self[elem], other[elem])
if newcount > 0:
result[elem] = newcount
return result
def __and__(self, other):
''' Intersection is the minimum of corresponding counts.
>>> Counter('abbb') & Counter('bcc')
Counter({'b': 1})
'''
if not isinstance(other, Counter):
return NotImplemented
_min = min
result = Counter()
if len(self) < len(other):
self, other = other, self
for elem in ifilter(self.__contains__, other):
newcount = _min(self[elem], other[elem])
if newcount > 0:
result[elem] = newcount
return result
if __name__ == '__main__':
import doctest
print(doctest.testmod())

View File

@@ -1,258 +0,0 @@
# Backport of OrderedDict() class that runs on Python 2.4, 2.5, 2.6, 2.7 and pypy.
# Passes Python2.7's test suite and incorporates all the latest updates.
try:
from thread import get_ident as _get_ident
except ImportError:
from dummy_thread import get_ident as _get_ident
try:
from _abcoll import KeysView, ValuesView, ItemsView
except ImportError:
pass
class OrderedDict(dict):
'Dictionary that remembers insertion order'
# An inherited dict maps keys to values.
# The inherited dict provides __getitem__, __len__, __contains__, and get.
# The remaining methods are order-aware.
# Big-O running times for all methods are the same as for regular dictionaries.
# The internal self.__map dictionary maps keys to links in a doubly linked list.
# The circular doubly linked list starts and ends with a sentinel element.
# The sentinel element never gets deleted (this simplifies the algorithm).
# Each link is stored as a list of length three: [PREV, NEXT, KEY].
def __init__(self, *args, **kwds):
'''Initialize an ordered dictionary. Signature is the same as for
regular dictionaries, but keyword arguments are not recommended
because their insertion order is arbitrary.
'''
if len(args) > 1:
raise TypeError('expected at most 1 arguments, got %d' % len(args))
try:
self.__root
except AttributeError:
self.__root = root = [] # sentinel node
root[:] = [root, root, None]
self.__map = {}
self.__update(*args, **kwds)
def __setitem__(self, key, value, dict_setitem=dict.__setitem__):
'od.__setitem__(i, y) <==> od[i]=y'
# Setting a new item creates a new link which goes at the end of the linked
# list, and the inherited dictionary is updated with the new key/value pair.
if key not in self:
root = self.__root
last = root[0]
last[1] = root[0] = self.__map[key] = [last, root, key]
dict_setitem(self, key, value)
def __delitem__(self, key, dict_delitem=dict.__delitem__):
'od.__delitem__(y) <==> del od[y]'
# Deleting an existing item uses self.__map to find the link which is
# then removed by updating the links in the predecessor and successor nodes.
dict_delitem(self, key)
link_prev, link_next, key = self.__map.pop(key)
link_prev[1] = link_next
link_next[0] = link_prev
def __iter__(self):
'od.__iter__() <==> iter(od)'
root = self.__root
curr = root[1]
while curr is not root:
yield curr[2]
curr = curr[1]
def __reversed__(self):
'od.__reversed__() <==> reversed(od)'
root = self.__root
curr = root[0]
while curr is not root:
yield curr[2]
curr = curr[0]
def clear(self):
'od.clear() -> None. Remove all items from od.'
try:
for node in self.__map.itervalues():
del node[:]
root = self.__root
root[:] = [root, root, None]
self.__map.clear()
except AttributeError:
pass
dict.clear(self)
def popitem(self, last=True):
'''od.popitem() -> (k, v), return and remove a (key, value) pair.
Pairs are returned in LIFO order if last is true or FIFO order if false.
'''
if not self:
raise KeyError('dictionary is empty')
root = self.__root
if last:
link = root[0]
link_prev = link[0]
link_prev[1] = root
root[0] = link_prev
else:
link = root[1]
link_next = link[1]
root[1] = link_next
link_next[0] = root
key = link[2]
del self.__map[key]
value = dict.pop(self, key)
return key, value
# -- the following methods do not depend on the internal structure --
def keys(self):
'od.keys() -> list of keys in od'
return list(self)
def values(self):
'od.values() -> list of values in od'
return [self[key] for key in self]
def items(self):
'od.items() -> list of (key, value) pairs in od'
return [(key, self[key]) for key in self]
def iterkeys(self):
'od.iterkeys() -> an iterator over the keys in od'
return iter(self)
def itervalues(self):
'od.itervalues -> an iterator over the values in od'
for k in self:
yield self[k]
def iteritems(self):
'od.iteritems -> an iterator over the (key, value) items in od'
for k in self:
yield (k, self[k])
def update(*args, **kwds):
'''od.update(E, **F) -> None. Update od from dict/iterable E and F.
If E is a dict instance, does: for k in E: od[k] = E[k]
If E has a .keys() method, does: for k in E.keys(): od[k] = E[k]
Or if E is an iterable of items, does: for k, v in E: od[k] = v
In either case, this is followed by: for k, v in F.items(): od[k] = v
'''
if len(args) > 2:
raise TypeError('update() takes at most 2 positional '
'arguments (%d given)' % (len(args),))
elif not args:
raise TypeError('update() takes at least 1 argument (0 given)')
self = args[0]
# Make progressively weaker assumptions about "other"
other = ()
if len(args) == 2:
other = args[1]
if isinstance(other, dict):
for key in other:
self[key] = other[key]
elif hasattr(other, 'keys'):
for key in other.keys():
self[key] = other[key]
else:
for key, value in other:
self[key] = value
for key, value in kwds.items():
self[key] = value
__update = update # let subclasses override update without breaking __init__
__marker = object()
def pop(self, key, default=__marker):
'''od.pop(k[,d]) -> v, remove specified key and return the corresponding value.
If key is not found, d is returned if given, otherwise KeyError is raised.
'''
if key in self:
result = self[key]
del self[key]
return result
if default is self.__marker:
raise KeyError(key)
return default
def setdefault(self, key, default=None):
'od.setdefault(k[,d]) -> od.get(k,d), also set od[k]=d if k not in od'
if key in self:
return self[key]
self[key] = default
return default
def __repr__(self, _repr_running={}):
'od.__repr__() <==> repr(od)'
call_key = id(self), _get_ident()
if call_key in _repr_running:
return '...'
_repr_running[call_key] = 1
try:
if not self:
return '%s()' % (self.__class__.__name__,)
return '%s(%r)' % (self.__class__.__name__, self.items())
finally:
del _repr_running[call_key]
def __reduce__(self):
'Return state information for pickling'
items = [[k, self[k]] for k in self]
inst_dict = vars(self).copy()
for k in vars(OrderedDict()):
inst_dict.pop(k, None)
if inst_dict:
return (self.__class__, (items,), inst_dict)
return self.__class__, (items,)
def copy(self):
'od.copy() -> a shallow copy of od'
return self.__class__(self)
@classmethod
def fromkeys(cls, iterable, value=None):
'''OD.fromkeys(S[, v]) -> New ordered dictionary with keys from S
and values equal to v (which defaults to None).
'''
d = cls()
for key in iterable:
d[key] = value
return d
def __eq__(self, other):
'''od.__eq__(y) <==> od==y. Comparison to another OD is order-sensitive
while comparison to a regular mapping is order-insensitive.
'''
if isinstance(other, OrderedDict):
return len(self)==len(other) and self.items() == other.items()
return dict.__eq__(self, other)
def __ne__(self, other):
return not self == other
# -- the following methods are only used in Python 2.7 --
def viewkeys(self):
"od.viewkeys() -> a set-like object providing a view on od's keys"
return KeysView(self)
def viewvalues(self):
"od.viewvalues() -> an object providing a view on od's values"
return ValuesView(self)
def viewitems(self):
"od.viewitems() -> a set-like object providing a view on od's items"
return ItemsView(self)

View File

@@ -1,22 +1,43 @@
import collections
import copy
import collections
import functools
import inspect
import os
import types
import six
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
class VCR(object):
def __init__(self, serializer='yaml', cassette_library_dir=None,
record_mode="once", filter_headers=(),
filter_query_parameters=(), before_record_request=None,
before_record_response=None, ignore_hosts=(),
match_on=('method', 'scheme', 'host', 'port', 'path', 'query',),
ignore_localhost=False, before_record=None):
@staticmethod
def is_test_method(method_name, function):
return method_name.startswith('test') and \
isinstance(function, types.FunctionType)
@staticmethod
def ensure_suffix(suffix):
def ensure(path):
if not path.endswith(suffix):
return path + suffix
return path
return ensure
def __init__(self, path_transformer=None, before_record_request=None,
custom_patches=(), filter_query_parameters=(), ignore_hosts=(),
record_mode="once", ignore_localhost=False, filter_headers=(),
before_record_response=None, filter_post_data_parameters=(),
match_on=('method', 'scheme', 'host', 'port', 'path', 'query'),
before_record=None, inject_cassette=False, serializer='yaml',
cassette_library_dir=None, func_path_generator=None,
decode_compressed_response=False):
self.serializer = serializer
self.match_on = match_on
self.cassette_library_dir = cassette_library_dir
@@ -34,24 +55,33 @@ class VCR(object):
'path': matchers.path,
'query': matchers.query,
'headers': matchers.headers,
'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
self.filter_post_data_parameters = filter_post_data_parameters
self.before_record_request = before_record_request or before_record
self.before_record_response = before_record_response
self.ignore_hosts = ignore_hosts
self.ignore_localhost = ignore_localhost
self.inject_cassette = inject_cassette
self.path_transformer = path_transformer
self.func_path_generator = func_path_generator
self.decode_compressed_response = decode_compressed_response
self._custom_patches = tuple(custom_patches)
def _get_serializer(self, serializer_name):
try:
serializer = self.serializers[serializer_name]
except KeyError:
print("Serializer {0} doesn't exist or isn't registered".format(
serializer_name
))
raise KeyError
raise KeyError(
"Serializer {} doesn't exist or isn't registered".format(
serializer_name
)
)
return serializer
def _get_matchers(self, matcher_names):
@@ -61,45 +91,94 @@ 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
def use_cassette(self, path, with_current_defaults=False, **kwargs):
def use_cassette(self, path=None, **kwargs):
if path is not None and not isinstance(path, six.string_types):
function = path
# Assume this is an attempt to decorate a function
return self._use_cassette(**kwargs)(function)
return self._use_cassette(path=path, **kwargs)
def _use_cassette(self, with_current_defaults=False, **kwargs):
if with_current_defaults:
return Cassette.use(path, self.get_path_and_merged_config(path, **kwargs))
args_getter = functools.partial(self.get_path_and_merged_config, path, **kwargs)
config = self.get_merged_config(**kwargs)
return Cassette.use(**config)
# This is made a function that evaluates every time a cassette
# is made so that changes that are made to this VCR instance
# that occur AFTER the `use_cassette` decorator is applied
# still affect subsequent calls to the decorated function.
args_getter = functools.partial(self.get_merged_config, **kwargs)
return Cassette.use_arg_getter(args_getter)
def get_path_and_merged_config(self, path, **kwargs):
def get_merged_config(self, **kwargs):
serializer_name = kwargs.get('serializer', self.serializer)
matcher_names = kwargs.get('match_on', self.match_on)
path_transformer = kwargs.get(
'path_transformer',
self.path_transformer
)
func_path_generator = kwargs.get(
'func_path_generator',
self.func_path_generator
)
cassette_library_dir = kwargs.get(
'cassette_library_dir',
self.cassette_library_dir
)
additional_matchers = kwargs.get('additional_matchers', ())
if cassette_library_dir:
path = os.path.join(cassette_library_dir, path)
def add_cassette_library_dir(path):
if not path.startswith(cassette_library_dir):
return os.path.join(cassette_library_dir, path)
return path
path_transformer = compose(
add_cassette_library_dir, path_transformer
)
elif not func_path_generator:
# If we don't have a library dir, use the functions
# location to build a full path for cassettes.
func_path_generator = self._build_path_from_func_using_module
merged_config = {
'serializer': self._get_serializer(serializer_name),
'match_on': self._get_matchers(matcher_names),
'persister': self.persister,
'match_on': self._get_matchers(
tuple(matcher_names) + tuple(additional_matchers)
),
'record_mode': kwargs.get('record_mode', self.record_mode),
'before_record_request': self._build_before_record_request(kwargs),
'before_record_response': self._build_before_record_response(kwargs)
'before_record_response': self._build_before_record_response(kwargs),
'custom_patches': self._custom_patches + kwargs.get(
'custom_patches', ()
),
'inject': kwargs.get('inject_cassette', self.inject_cassette),
'path_transformer': path_transformer,
'func_path_generator': func_path_generator
}
return path, merged_config
path = kwargs.get('path')
if path:
merged_config['path'] = path
return merged_config
def _build_before_record_response(self, options):
before_record_response = options.get(
'before_record_response', self.before_record_response
)
decode_compressed_response = options.get(
'decode_compressed_response', self.decode_compressed_response
)
filter_functions = []
if before_record_response and not isinstance(before_record_response,
collections.Iterable):
before_record_response = (before_record_response,)
for function in before_record_response:
filter_functions.append(function)
if decode_compressed_response:
filter_functions.append(filters.decode_response)
if before_record_response:
if not isinstance(before_record_response, collections.Iterable):
before_record_response = (before_record_response,)
filter_functions.extend(before_record_response)
def before_record_response(response):
for function in filter_functions:
if response is None:
@@ -116,8 +195,12 @@ class VCR(object):
filter_query_parameters = options.get(
'filter_query_parameters', self.filter_query_parameters
)
filter_post_data_parameters = options.get(
'filter_post_data_parameters', self.filter_post_data_parameters
)
before_record_request = options.get(
"before_record_request", options.get("before_record", self.before_record_request)
"before_record_request",
options.get("before_record", self.before_record_request)
)
ignore_hosts = options.get(
'ignore_hosts', self.ignore_hosts
@@ -126,25 +209,42 @@ class VCR(object):
'ignore_localhost', self.ignore_localhost
)
if filter_headers:
filter_functions.append(functools.partial(filters.remove_headers,
headers_to_remove=filter_headers))
replacements = [h if isinstance(h, tuple) else (h, None)
for h in filter_headers]
filter_functions.append(
functools.partial(
filters.replace_headers,
replacements=replacements,
)
)
if filter_query_parameters:
filter_functions.append(functools.partial(filters.remove_query_parameters,
query_parameters_to_remove=filter_query_parameters))
replacements = [p if isinstance(p, tuple) else (p, None)
for p in filter_query_parameters]
filter_functions.append(functools.partial(
filters.replace_query_parameters,
replacements=replacements,
))
if filter_post_data_parameters:
replacements = [p if isinstance(p, tuple) else (p, None)
for p in filter_post_data_parameters]
filter_functions.append(
functools.partial(
filters.replace_post_data_parameters,
replacements=replacements,
)
)
hosts_to_ignore = list(ignore_hosts)
hosts_to_ignore = set(ignore_hosts)
if ignore_localhost:
hosts_to_ignore.extend(('localhost', '0.0.0.0', '127.0.0.1'))
hosts_to_ignore.update(('localhost', '0.0.0.0', '127.0.0.1'))
if hosts_to_ignore:
hosts_to_ignore = set(hosts_to_ignore)
filter_functions.append(self._build_ignore_hosts(hosts_to_ignore))
if before_record_request:
if not isinstance(before_record_request, collections.Iterable):
before_record_request = (before_record_request,)
for function in before_record_request:
filter_functions.append(function)
filter_functions.extend(before_record_request)
def before_record_request(request):
request = copy.copy(request)
for function in filter_functions:
@@ -152,7 +252,6 @@ class VCR(object):
break
request = function(request)
return request
return before_record_request
@staticmethod
@@ -163,8 +262,21 @@ class VCR(object):
return request
return filter_ignored_hosts
@staticmethod
def _build_path_from_func_using_module(function):
return os.path.join(os.path.dirname(inspect.getfile(function)),
function.__name__)
def register_serializer(self, name, serializer):
self.serializers[name] = serializer
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))

View File

@@ -3,8 +3,5 @@ class CannotOverwriteExistingCassetteException(Exception):
class UnhandledHTTPRequestError(KeyError):
'''
Raised when a cassette does not c
ontain the request we want
'''
"""Raised when a cassette does not contain the request we want."""
pass

View File

@@ -1,24 +1,163 @@
from six import BytesIO, text_type
from six.moves.urllib.parse import urlparse, urlencode, urlunparse
import copy
import json
import zlib
from .util import CaseInsensitiveDict
def replace_headers(request, replacements):
"""
Replace headers in request according to replacements. The replacements
should be a list of (key, value) pairs where the value can be any of:
1. A simple replacement string value.
2. None to remove the given header.
3. A callable which accepts (key, value, request) and returns a string
value or None.
"""
new_headers = request.headers.copy()
for k, rv in replacements:
if k in new_headers:
ov = new_headers.pop(k)
if callable(rv):
rv = rv(key=k, value=ov, request=request)
if rv is not None:
new_headers[k] = rv
request.headers = new_headers
return request
def remove_headers(request, headers_to_remove):
headers = copy.copy(request.headers)
headers_to_remove = [h.lower() for h in headers_to_remove]
keys = [k for k in headers if k.lower() in headers_to_remove]
if keys:
for k in keys:
headers.pop(k)
request.headers = headers
"""
Wrap replace_headers() for API backward compatibility.
"""
replacements = [(k, None) for k in headers_to_remove]
return replace_headers(request, replacements)
def replace_query_parameters(request, replacements):
"""
Replace query parameters in request according to replacements. The
replacements should be a list of (key, value) pairs where the value can be
any of:
1. A simple replacement string value.
2. None to remove the given header.
3. A callable which accepts (key, value, request) and returns a string
value or None.
"""
query = request.query
new_query = []
replacements = dict(replacements)
for k, ov in query:
if k not in replacements:
new_query.append((k, ov))
else:
rv = replacements[k]
if callable(rv):
rv = rv(key=k, value=ov, request=request)
if rv is not None:
new_query.append((k, rv))
uri_parts = list(urlparse(request.uri))
uri_parts[4] = urlencode(new_query)
request.uri = urlunparse(uri_parts)
return request
def remove_query_parameters(request, query_parameters_to_remove):
query = request.query
new_query = [(k, v) for (k, v) in query
if k not in query_parameters_to_remove]
if len(new_query) != len(query):
uri_parts = list(urlparse(request.uri))
uri_parts[4] = urlencode(new_query)
request.uri = urlunparse(uri_parts)
"""
Wrap replace_query_parameters() for API backward compatibility.
"""
replacements = [(k, None) for k in query_parameters_to_remove]
return replace_query_parameters(request, replacements)
def replace_post_data_parameters(request, replacements):
"""
Replace post data in request--either form data or json--according to
replacements. The replacements should be a list of (key, value) pairs where
the value can be any of:
1. A simple replacement string value.
2. None to remove the given header.
3. A callable which accepts (key, value, request) and returns a string
value or None.
"""
replacements = dict(replacements)
if request.method == 'POST' and not isinstance(request.body, BytesIO):
if request.headers.get('Content-Type') == 'application/json':
json_data = json.loads(request.body.decode('utf-8'))
for k, rv in replacements.items():
if k in json_data:
ov = json_data.pop(k)
if callable(rv):
rv = rv(key=k, value=ov, request=request)
if rv is not None:
json_data[k] = rv
request.body = json.dumps(json_data).encode('utf-8')
else:
if isinstance(request.body, text_type):
request.body = request.body.encode('utf-8')
splits = [p.partition(b'=') for p in request.body.split(b'&')]
new_splits = []
for k, sep, ov in splits:
if sep is None:
new_splits.append((k, sep, ov))
else:
rk = k.decode('utf-8')
if rk not in replacements:
new_splits.append((k, sep, ov))
else:
rv = replacements[rk]
if callable(rv):
rv = rv(key=rk, value=ov.decode('utf-8'),
request=request)
if rv is not None:
new_splits.append((k, sep, rv.encode('utf-8')))
request.body = b'&'.join(k if sep is None else b''.join([k, sep, v])
for k, sep, v in new_splits)
return request
def remove_post_data_parameters(request, post_data_parameters_to_remove):
"""
Wrap replace_post_data_parameters() for API backward compatibility.
"""
replacements = [(k, None) for k in post_data_parameters_to_remove]
return replace_post_data_parameters(request, replacements)
def decode_response(response):
"""
If the response is compressed with gzip or deflate:
1. decompress the response body
2. delete the content-encoding header
3. update content-length header to decompressed length
"""
def is_compressed(headers):
encoding = headers.get('content-encoding', [])
return encoding and encoding[0] in ('gzip', 'deflate')
def decompress_body(body, encoding):
"""Returns decompressed body according to encoding using zlib.
to (de-)compress gzip format, use wbits = zlib.MAX_WBITS | 16
"""
if encoding == 'gzip':
return zlib.decompress(body, zlib.MAX_WBITS | 16)
else: # encoding == 'deflate'
return zlib.decompress(body)
# Deepcopy here in case `headers` contain objects that could
# be mutated by a shallow copy and corrupt the real response.
response = copy.deepcopy(response)
headers = CaseInsensitiveDict(response['headers'])
if is_compressed(headers):
encoding = headers['content-encoding'][0]
headers['content-encoding'].remove(encoding)
if not headers['content-encoding']:
del headers['content-encoding']
new_body = decompress_body(response['body']['string'], encoding)
response['body']['string'] = new_body
headers['content-length'] = [str(len(new_body))]
response['headers'] = dict(headers)
return response

View File

@@ -1,4 +1,9 @@
import json
from six.moves import urllib, xmlrpc_client
from .util import read_body
import logging
log = logging.getLogger(__name__)
@@ -30,8 +35,51 @@ def query(r1, r2):
return r1.query == r2.query
def raw_body(r1, r2):
return read_body(r1) == read_body(r2)
def _header_checker(value, header='Content-Type'):
def checker(headers):
return value in headers.get(header, '').lower()
return checker
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).
if body:
return json.loads(body.decode('utf-8'))
_xml_header_checker = _header_checker('text/xml')
_xmlrpc_header_checker = _header_checker('xmlrpc', header='User-Agent')
_checker_transformer_pairs = (
(_header_checker('application/x-www-form-urlencoded'), urllib.parse.parse_qs),
(_header_checker('application/json'), _transform_json),
(lambda request: _xml_header_checker(request) and _xmlrpc_header_checker(request), xmlrpc_client.loads),
)
def _identity(x):
return x
def _get_transformer(request):
for checker, transformer in _checker_transformer_pairs:
if checker(request.headers):
return transformer
else:
return _identity
def body(r1, r2):
return r1.body == r2.body
transformer = _get_transformer(r1)
r2_transformer = _get_transformer(r2)
if transformer != r2_transformer:
transformer = _identity
return transformer(read_body(r1)) == transformer(read_body(r2))
def headers(r1, r2):
@@ -42,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)

View File

@@ -19,7 +19,7 @@ import sys
import tempfile
import yaml
from .serializers import compat, yamlserializer, jsonserializer
from .serializers import yamlserializer, jsonserializer
from .serialize import serialize
from . import request
from .stubs.compat import get_httpmessage
@@ -58,8 +58,8 @@ PARTS = [
def build_uri(**parts):
port = parts['port']
scheme = parts['protocol']
default_port = {'https': 433, 'http': 80}[scheme]
parts['port'] = ':{0}'.format(port) if port != default_port else ''
default_port = {'https': 443, 'http': 80}[scheme]
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()

View File

@@ -2,9 +2,7 @@
import functools
import itertools
import contextlib2
import mock
from .compat import contextlib, mock
from .stubs import VCRHTTPConnection, VCRHTTPSConnection
from six.moves import http_client as httplib
@@ -14,9 +12,20 @@ _HTTPConnection = httplib.HTTPConnection
_HTTPSConnection = httplib.HTTPSConnection
# Try to save the original types for requests
# Try to save the original types for boto3
try:
import requests.packages.urllib3.connectionpool as cpool
import botocore.vendored.requests.packages.urllib3.connectionpool as cpool
except ImportError: # pragma: no cover
pass
else:
_Boto3VerifiedHTTPSConnection = cpool.VerifiedHTTPSConnection
_cpoolBoto3HTTPConnection = cpool.HTTPConnection
_cpoolBoto3HTTPSConnection = cpool.HTTPSConnection
cpool = None
# Try to save the original types for urllib3
try:
import urllib3.connectionpool as cpool
except ImportError: # pragma: no cover
pass
else:
@@ -24,14 +33,16 @@ else:
_cpoolHTTPConnection = cpool.HTTPConnection
_cpoolHTTPSConnection = cpool.HTTPSConnection
# Try to save the original types for urllib3
# Try to save the original types for requests
try:
import urllib3
if not cpool:
import requests.packages.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 httplib2
@@ -54,6 +65,32 @@ else:
_CertValidatingHTTPSConnection = boto.https_connection.CertValidatingHTTPSConnection
# Try to save the original types for Tornado
try:
import tornado.simple_httpclient
except ImportError: # pragma: no cover
pass
else:
_SimpleAsyncHTTPClient_fetch_impl = \
tornado.simple_httpclient.SimpleAsyncHTTPClient.fetch_impl
try:
import tornado.curl_httpclient
except ImportError: # pragma: no cover
pass
else:
_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):
def _build_patchers_from_mock_triples_decorator(function):
@@ -69,9 +106,13 @@ class CassettePatcherBuilder(object):
self._class_to_cassette_subclass = {}
def build(self):
return itertools.chain(self._httplib(), self._requests(),
self._urllib3(), self._httplib2(),
self._boto())
return itertools.chain(
self._httplib(), self._requests(), self._boto3(), self._urllib3(),
self._httplib2(), self._boto(), self._tornado(), self._aiohttp(),
self._build_patchers_from_mock_triples(
self._cassette.custom_patches
),
)
def _build_patchers_from_mock_triples(self, mock_triples):
for args in mock_triples:
@@ -88,14 +129,31 @@ class CassettePatcherBuilder(object):
replacement_class))
def _recursively_apply_get_cassette_subclass(self, replacement_dict_or_obj):
"""One of the subtleties of this class is that it does not directly
replace HTTPSConnection with `VCRRequestsHTTPSConnection`, but a
subclass of the aforementioned class that has the `cassette`
class attribute assigned to `self._cassette`. This behavior is
necessary to properly support nested cassette contexts.
This function exists to ensure that we use the same class
object (reference) to patch everything that replaces
VCRRequestHTTP[S]Connection, but that we can talk about
patching them with the raw references instead, and without
worrying about exactly where the subclass with the relevant
value for `cassette` is first created.
The function is recursive because it looks in to dictionaries
and replaces class values at any depth with the subclass
described in the previous paragraph.
"""
if isinstance(replacement_dict_or_obj, dict):
for key, replacement_obj in replacement_dict_or_obj.items():
for key, replacement_obj in replacement_dict_or_obj.items():
replacement_obj = self._recursively_apply_get_cassette_subclass(
replacement_obj)
replacement_dict_or_obj[key] = replacement_obj
return replacement_dict_or_obj
if hasattr(replacement_dict_or_obj, 'cassette'):
replacement_dict_or_obj = self._get_cassette_subclass(
replacement_dict_or_obj = self._get_cassette_subclass(
replacement_dict_or_obj)
return replacement_dict_or_obj
@@ -109,9 +167,9 @@ class CassettePatcherBuilder(object):
def _build_cassette_subclass(self, base_class):
bases = (base_class,)
if not issubclass(base_class, object): # Check for old style 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
@@ -121,74 +179,58 @@ 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.requests_stubs import VCRRequestsHTTPConnection, VCRRequestsHTTPSConnection
http_connection_remover = ConnectionRemover(
self._get_cassette_subclass(VCRRequestsHTTPConnection)
)
https_connection_remover = ConnectionRemover(
self._get_cassette_subclass(VCRRequestsHTTPSConnection)
)
mock_triples = (
(cpool, 'VerifiedHTTPSConnection', VCRRequestsHTTPSConnection),
(cpool, 'VerifiedHTTPSConnection', VCRRequestsHTTPSConnection),
(cpool, 'HTTPConnection', VCRRequestsHTTPConnection),
(cpool, 'HTTPSConnection', VCRRequestsHTTPSConnection),
(cpool.HTTPConnectionPool, 'ConnectionCls', VCRRequestsHTTPConnection),
(cpool.HTTPSConnectionPool, 'ConnectionCls', VCRRequestsHTTPSConnection),
)
# These handle making sure that sessions only use the
# connections of the appropriate type.
mock_triples += ((cpool.HTTPConnectionPool, '_get_conn',
self._patched_get_conn(cpool.HTTPConnectionPool,
lambda : cpool.HTTPConnection)),
(cpool.HTTPSConnectionPool, '_get_conn',
self._patched_get_conn(cpool.HTTPSConnectionPool,
lambda : cpool.HTTPSConnection)),
(cpool.HTTPConnectionPool, '_new_conn',
self._patched_new_conn(cpool.HTTPConnectionPool,
http_connection_remover)),
(cpool.HTTPSConnectionPool, '_new_conn',
self._patched_new_conn(cpool.HTTPSConnectionPool,
https_connection_remover)))
return self._urllib3_patchers(cpool, requests_stubs)
return itertools.chain(self._build_patchers_from_mock_triples(mock_triples),
(http_connection_remover, https_connection_remover))
def _boto3(self):
try:
import botocore.vendored.requests.packages.urllib3.connectionpool as cpool
except ImportError: # pragma: no cover
return ()
from .stubs import boto3_stubs
return self._urllib3_patchers(cpool, boto3_stubs)
def _patched_get_conn(self, connection_pool_class, connection_class_getter):
get_conn = connection_pool_class._get_conn
@functools.wraps(get_conn)
def patched_get_conn(pool, timeout=None):
connection = get_conn(pool, timeout)
connection_class = pool.ConnectionCls if hasattr(pool, 'ConnectionCls') \
else connection_class_getter()
connection_class = (
pool.ConnectionCls if hasattr(pool, 'ConnectionCls')
else connection_class_getter())
# We need to make sure that we are actually providing a
# patched version of the connection class. This might not
# always be the case because the pool keeps previously
# used connections (which might actually be of a different
# class) around. This while loop will terminate because
# eventually the pool will run out of connections.
while not isinstance(connection, connection_class):
connection = get_conn(pool, timeout)
return connection
return patched_get_conn
def _patched_new_conn(self, connection_pool_class, connection_remover):
new_conn = connection_pool_class._new_conn
@functools.wraps(new_conn)
def patched_new_conn(pool):
new_connection = new_conn(pool)
connection_remover.add_connection_to_pool_entry(pool, new_connection)
return new_connection
return patched_new_conn
@_build_patchers_from_mock_triples_decorator
def _urllib3(self):
try:
import urllib3.connectionpool as cpool
except ImportError: # pragma: no cover
pass
else:
from .stubs.urllib3_stubs import VCRVerifiedHTTPSConnection
yield cpool, 'VerifiedHTTPSConnection', VCRVerifiedHTTPSConnection
yield cpool, 'HTTPConnection', VCRHTTPConnection
return ()
from .stubs import urllib3_stubs
return self._urllib3_patchers(cpool, urllib3_stubs)
@_build_patchers_from_mock_triples_decorator
def _httplib2(self):
@@ -215,6 +257,77 @@ class CassettePatcherBuilder(object):
from .stubs.boto_stubs import VCRCertValidatingHTTPSConnection
yield cpool, 'CertValidatingHTTPSConnection', VCRCertValidatingHTTPSConnection
@_build_patchers_from_mock_triples_decorator
def _tornado(self):
try:
import tornado.simple_httpclient as simple
except ImportError: # pragma: no cover
pass
else:
from .stubs.tornado_stubs import vcr_fetch_impl
new_fetch_impl = vcr_fetch_impl(
self._cassette, _SimpleAsyncHTTPClient_fetch_impl
)
yield simple.SimpleAsyncHTTPClient, 'fetch_impl', new_fetch_impl
try:
import tornado.curl_httpclient as curl
except ImportError: # pragma: no cover
pass
else:
from .stubs.tornado_stubs import vcr_fetch_impl
new_fetch_impl = vcr_fetch_impl(
self._cassette, _CurlAsyncHTTPClient_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):
http_connection_remover = ConnectionRemover(
self._get_cassette_subclass(stubs.VCRRequestsHTTPConnection)
)
https_connection_remover = ConnectionRemover(
self._get_cassette_subclass(stubs.VCRRequestsHTTPSConnection)
)
mock_triples = (
(cpool, 'VerifiedHTTPSConnection', stubs.VCRRequestsHTTPSConnection),
(cpool, 'HTTPConnection', stubs.VCRRequestsHTTPConnection),
(cpool, 'HTTPSConnection', stubs.VCRRequestsHTTPSConnection),
(cpool, 'is_connection_dropped', mock.Mock(return_value=False)), # Needed on Windows only
(cpool.HTTPConnectionPool, 'ConnectionCls', stubs.VCRRequestsHTTPConnection),
(cpool.HTTPSConnectionPool, 'ConnectionCls', stubs.VCRRequestsHTTPSConnection),
)
# These handle making sure that sessions only use the
# connections of the appropriate type.
mock_triples += ((cpool.HTTPConnectionPool, '_get_conn',
self._patched_get_conn(cpool.HTTPConnectionPool,
lambda: cpool.HTTPConnection)),
(cpool.HTTPSConnectionPool, '_get_conn',
self._patched_get_conn(cpool.HTTPSConnectionPool,
lambda: cpool.HTTPSConnection)),
(cpool.HTTPConnectionPool, '_new_conn',
self._patched_new_conn(cpool.HTTPConnectionPool,
http_connection_remover)),
(cpool.HTTPSConnectionPool, '_new_conn',
self._patched_new_conn(cpool.HTTPSConnectionPool,
https_connection_remover)))
return itertools.chain(self._build_patchers_from_mock_triples(mock_triples),
(http_connection_remover, https_connection_remover))
class ConnectionRemover(object):
@@ -236,7 +349,7 @@ class ConnectionRemover(object):
def __exit__(self, *args):
for pool, connections in self._connection_pool_to_connections.items():
readd_connections = []
while not pool.pool.empty() and connections:
while pool.pool and not pool.pool.empty() and connections:
connection = pool.pool.get()
if isinstance(connection, self._connection_class):
connections.remove(connection)
@@ -249,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:
@@ -273,11 +400,29 @@ 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
except ImportError: # pragma: no cover
pass
else:
# unpatch requests v1.x
yield mock.patch.object(cpool, 'VerifiedHTTPSConnection', _Boto3VerifiedHTTPSConnection)
yield mock.patch.object(cpool, 'HTTPConnection', _cpoolBoto3HTTPConnection)
# unpatch requests v2.x
if hasattr(cpool.HTTPConnectionPool, 'ConnectionCls'):
yield mock.patch.object(cpool.HTTPConnectionPool, 'ConnectionCls',
_cpoolBoto3HTTPConnection)
yield mock.patch.object(cpool.HTTPSConnectionPool, 'ConnectionCls',
_cpoolBoto3HTTPSConnection)
if hasattr(cpool, 'HTTPSConnection'):
yield mock.patch.object(cpool, 'HTTPSConnection', _cpoolBoto3HTTPSConnection)
try:
import httplib2 as cpool
@@ -296,10 +441,31 @@ def reset_patchers():
yield mock.patch.object(cpool, 'CertValidatingHTTPSConnection',
_CertValidatingHTTPSConnection)
try:
import tornado.simple_httpclient as simple
except ImportError: # pragma: no cover
pass
else:
yield mock.patch.object(
simple.SimpleAsyncHTTPClient,
'fetch_impl',
_SimpleAsyncHTTPClient_fetch_impl,
)
try:
import tornado.curl_httpclient as curl
except ImportError: # pragma: no cover
pass
else:
yield mock.patch.object(
curl.CurlAsyncHTTPClient,
'fetch_impl',
_CurlAsyncHTTPClient_fetch_impl,
)
@contextlib2.contextmanager
@contextlib.contextmanager
def force_reset():
with contextlib2.ExitStack() as exit_stack:
with contextlib.ExitStack() as exit_stack:
for patcher in reset_patchers():
exit_stack.enter_context(patcher)
yield

View File

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

View File

@@ -1,9 +1,24 @@
# .. _persister_example:
import os
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)

View File

@@ -1,42 +1,49 @@
import warnings
from six import BytesIO, text_type
from six.moves.urllib.parse import urlparse, parse_qsl
from .util import CaseInsensitiveDict
class Request(object):
"""
VCR's representation of a request.
There is a weird quirk in HTTP. You can send the same header twice. For
this reason, headers are represented by a dict, with lists as the values.
However, it appears that HTTPlib is completely incapable of sending the
same header twice. This puts me in a weird position: I want to be able to
accurately represent HTTP headers in cassettes, but I don't want the extra
step of always having to do [0] in the general case, i.e.
request.headers['key'][0]
In addition, some servers sometimes send the same header more than once,
and httplib *can* deal with this situation.
Futhermore, I wanted to keep the request and response cassette format as
similar as possible.
For this reason, in cassettes I keep a dict with lists as keys, but once
deserialized into VCR, I keep them as plain, naked dicts.
VCR's representation of a request.
"""
def __init__(self, method, uri, body, headers):
self.method = method
self.uri = uri
self.body = body
self.headers = {}
for key in headers:
self.add_header(key, headers[key])
self._was_file = hasattr(body, 'read')
if self._was_file:
self.body = body.read()
else:
self.body = body
self.headers = headers
@property
def headers(self):
return self._headers
@headers.setter
def headers(self, value):
if not isinstance(value, HeadersDict):
value = HeadersDict(value)
self._headers = value
@property
def body(self):
return BytesIO(self._body) if self._was_file else self._body
@body.setter
def body(self, value):
if isinstance(value, text_type):
value = value.encode('utf-8')
self._body = value
def add_header(self, key, value):
# see class docstring for an explanation
if isinstance(value, (tuple, list)):
self.headers[key] = value[0]
else:
self.headers[key] = value
warnings.warn("Request.add_header is deprecated. "
"Please assign to request.headers instead.",
DeprecationWarning)
self.headers[key] = value
@property
def scheme(self):
@@ -51,7 +58,7 @@ class Request(object):
parse_uri = urlparse(self.uri)
port = parse_uri.port
if port is None:
port = {'https': 433, 'http': 80}[parse_uri.scheme]
port = {'https': 443, 'http': 80}[parse_uri.scheme]
return port
@property
@@ -74,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__()
@@ -90,3 +97,35 @@ class Request(object):
@classmethod
def _from_dict(cls, dct):
return Request(**dct)
class HeadersDict(CaseInsensitiveDict):
"""
There is a weird quirk in HTTP. You can send the same header twice. For
this reason, headers are represented by a dict, with lists as the values.
However, it appears that HTTPlib is completely incapable of sending the
same header twice. This puts me in a weird position: I want to be able to
accurately represent HTTP headers in cassettes, but I don't want the extra
step of always having to do [0] in the general case, i.e.
request.headers['key'][0]
In addition, some servers sometimes send the same header more than once,
and httplib *can* deal with this situation.
Futhermore, I wanted to keep the request and response cassette format as
similar as possible.
For this reason, in cassettes I keep a dict with lists as keys, but once
deserialized into VCR, I keep them as plain, naked dicts.
"""
def __setitem__(self, key, value):
if isinstance(value, (tuple, list)):
value = value[0]
# Preserve the case from the first time this key was set.
old = self._store.get(key.lower())
if old:
key = old[0]
super(HeadersDict, self).__setitem__(key, value)

View File

@@ -50,7 +50,7 @@ def deserialize(cassette_string, serializer):
def serialize(cassette_dict, serializer):
interactions = ([{
'request': request._to_dict(),
'request': compat.convert_to_unicode(request._to_dict()),
'response': compat.convert_to_unicode(response),
} for request, response in zip(
cassette_dict['requests'],

View File

@@ -17,14 +17,14 @@ def convert_body_to_bytes(resp):
By default yaml serializes to utf-8 encoded bytestrings.
When this cassette is loaded by python3, it's automatically decoded
into unicode strings. This makes sure that it stays a bytestring, since
into unicode strings. This makes sure that it stays a bytestring, since
that's what all the internal httplib machinery is expecting.
For more info on py3 yaml:
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
@@ -37,19 +37,43 @@ def convert_body_to_bytes(resp):
return resp
def _convert_string_to_unicode(string):
"""
If the string is bytes, decode it to a string (for python3 support)
"""
result = string
try:
if string is not None and not isinstance(string, six.text_type):
result = string.decode('utf-8')
except (TypeError, UnicodeDecodeError, AttributeError):
# Sometimes the string actually is binary or StringIO object,
# so if you can't decode it, just give up.
pass
return result
def convert_body_to_unicode(resp):
"""
If the request body is bytes, decode it to a string (for python3 support)
If the request or responses body is bytes, decode it to a string
(for python3 support)
"""
try:
if not isinstance(resp['body']['string'], six.text_type):
resp['body']['string'] = resp['body']['string'].decode('utf-8')
except (KeyError, TypeError, UnicodeDecodeError):
# The thing we were converting either wasn't a dictionary or didn't
# have the keys we were expecting. Some of the tests just serialize
# and deserialize a string.
if type(resp) is not dict:
# Some of the tests just serialize and deserialize a string.
return _convert_string_to_unicode(resp)
else:
body = resp.get('body')
if body is not None:
try:
body['string'] = _convert_string_to_unicode(
body['string']
)
except (KeyError, TypeError, AttributeError):
# The thing we were converting either wasn't a dictionary or
# didn't have the keys we were expecting.
# For example request object has no 'string' key.
resp['body'] = _convert_string_to_unicode(body)
# Also, sometimes the thing actually is binary, so if you can't decode
# it, just give up.
pass
return resp

View File

@@ -9,16 +9,21 @@ def deserialize(cassette_string):
def serialize(cassette_dict):
error_message = (
"Does this HTTP interaction contain binary data? "
"If so, use a different serializer (like the yaml serializer) "
"for this request?"
)
try:
return json.dumps(cassette_dict, indent=4)
except UnicodeDecodeError as original:
except UnicodeDecodeError as original: # py2
raise UnicodeDecodeError(
original.encoding,
b"Error serializing cassette to JSON",
original.start,
original.end,
original.args[-1] +
("Does this HTTP interaction contain binary data? "
"If so, use a different serializer (like the yaml serializer) "
"for this request?")
original.args[-1] + error_message
)
except TypeError as original: # py3
raise TypeError(error_message)

View File

@@ -1,15 +1,10 @@
'''Stubs for patching HTTP and HTTPS requests'''
try:
import http.client
except ImportError:
pass
import logging
import six
from six.moves.http_client import (
HTTPConnection,
HTTPSConnection,
HTTPMessage,
HTTPResponse,
)
from six import BytesIO
@@ -23,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.
"""
@@ -76,7 +71,15 @@ class VCRHTTPResponse(HTTPResponse):
self._closed = False
headers = self.recorded_response['headers']
self.msg = parse_headers(headers)
# Since we are loading a response that has already been serialized, our
# response is no longer chunked. That means we don't want any
# libraries trying to process a chunked response. By removing the
# transfer-encoding: chunked header, this should cause the downstream
# libraries to process this as a non-chunked response.
te_key = [h for h in headers.keys() if h.upper() == 'TRANSFER-ENCODING']
if te_key:
del headers[te_key[0]]
self.headers = self.msg = parse_headers(headers)
self.length = compat.get_header(self.msg, 'content-length') or None
@@ -128,11 +131,14 @@ class VCRConnection(object):
Returns empty string for the default port and ':port' otherwise
"""
port = self.real_connection.port
default_port = {'https': 433, 'http': 80}[self._protocol]
return ':{0}'.format(port) if port != default_port else ''
default_port = {'https': 443, 'http': 80}[self._protocol]
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,
@@ -143,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,
@@ -158,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
@@ -177,11 +185,10 @@ 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):
for value in values:
self._vcr_request.add_header(header, value)
self._vcr_request.headers[header] = values
def send(self, data):
'''
@@ -189,28 +196,30 @@ class VCRConnection(object):
body of the request. So if that happens, let's just append the data
onto the most recent request in the cassette.
'''
self._vcr_request.body = (self._vcr_request.body or '') + data
self._vcr_request.body = self._vcr_request.body + data \
if self._vcr_request.body else data
def close(self):
# Note: the real connection will only close if it's open, so
# no need to check that here.
self.real_connection.close()
def endheaders(self, *args, **kwargs):
def endheaders(self, message_body=None):
"""
Normally, this would atually send the request to the server.
Normally, this would actually send the request to the server.
We are not sending the request until getting the response,
so bypass this method for now.
so bypass this part and just append the message body, if any.
"""
pass
if message_body is not None:
self._vcr_request.body = message_body
def getresponse(self, _=False):
def getresponse(self, _=False, **kwargs):
'''Retrieve the response'''
# Check to see if the cassette has a response for this request. If so,
# 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
)
)
@@ -232,16 +241,20 @@ 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
)
)
self.real_connection.request(
method=self._vcr_request.method,
url=self._url(self._vcr_request.uri),
body=self._vcr_request.body,
headers=self._vcr_request.headers,
)
# This is imported here to avoid circular import.
# TODO(@IvanMalison): Refactor to allow normal import.
from vcr.patch import force_reset
with force_reset():
self.real_connection.request(
method=self._vcr_request.method,
url=self._url(self._vcr_request.uri),
body=self._vcr_request.body,
headers=self._vcr_request.headers,
)
# get the response
response = self.real_connection.getresponse()
@@ -279,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):
@@ -294,7 +311,7 @@ class VCRConnection(object):
def __init__(self, *args, **kwargs):
if six.PY3:
kwargs.pop('strict', None) # apparently this is gone in py3
kwargs.pop('strict', None) # apparently this is gone in py3
# need to temporarily reset here because the real connection
# inherits from the thing that we are mocking out. Take out
@@ -303,6 +320,46 @@ 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
VCRConnection need to be propogated to the real connection.
For example, urllib3 will set certain attributes on the connection,
such as 'ssl_version'. These attributes need to get set on the real
connection to have the correct and expected behavior.
TODO: Separately setting the attribute on the two instances is not
ideal. We should switch to a proxying implementation.
"""
try:
setattr(self.real_connection, name, value)
except AttributeError:
# raised if real_connection has not been set yet, such as when
# we're setting the real_connection itself for the first time
pass
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'''
@@ -314,3 +371,4 @@ class VCRHTTPSConnection(VCRConnection):
'''A Mocked class for HTTPS requests'''
_baseclass = HTTPSConnection
_protocol = 'https'
is_verified = True

View File

@@ -0,0 +1,94 @@
'''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):
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,
)
async def json(self, *, encoding='utf-8', loads=json.loads, **kwargs): # NOQA: E999
return loads(self._body.decode(encoding))
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)
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')
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, URL(vcr_response.get('url')))
response.status = vcr_response['status']['code']
response._body = 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._body = msg.encode()
response.close()
return response
response = await real_request(self, method, url, **kwargs) # NOQA: E999
vcr_response = {
'status': {
'code': response.status,
'message': response.reason,
},
'headers': dict(response.headers),
'body': {'string': (await response.read())}, # NOQA: E999
'url': response.url,
}
cassette.append(vcr_request, vcr_response)
return response
return new_request

15
vcr/stubs/boto3_stubs.py Normal file
View File

@@ -0,0 +1,15 @@
'''Stubs for boto3'''
from botocore.vendored.requests.packages.urllib3.connectionpool import HTTPConnection, VerifiedHTTPSConnection
from ..stubs import VCRHTTPConnection, VCRHTTPSConnection
# urllib3 defines its own HTTPConnection classes, which boto3 goes ahead and assumes
# you're using. It includes some polyfills for newer features missing in older pythons.
class VCRRequestsHTTPConnection(VCRHTTPConnection, HTTPConnection):
_baseclass = HTTPConnection
class VCRRequestsHTTPSConnection(VCRHTTPSConnection, VerifiedHTTPSConnection):
_baseclass = VerifiedHTTPSConnection

View File

@@ -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',
@@ -41,7 +39,9 @@ class VCRHTTPSConnectionWithTimeout(VCRHTTPSConnection,
'strict',
'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:

View File

@@ -1,13 +1,19 @@
'''Stubs for requests'''
from requests.packages.urllib3.connectionpool import HTTPConnection, VerifiedHTTPSConnection
try:
from urllib3.connectionpool import HTTPConnection, VerifiedHTTPSConnection
except ImportError:
from requests.packages.urllib3.connectionpool import HTTPConnection, VerifiedHTTPSConnection
from ..stubs import VCRHTTPConnection, VCRHTTPSConnection
# urllib3 defines its own HTTPConnection classes, which requests goes ahead and assumes
# you're using. It includes some polyfills for newer features missing in older pythons.
class VCRRequestsHTTPConnection(VCRHTTPConnection, HTTPConnection):
_baseclass = HTTPConnection
class VCRRequestsHTTPSConnection(VCRHTTPSConnection, VerifiedHTTPSConnection):
_baseclass = VerifiedHTTPSConnection

107
vcr/stubs/tornado_stubs.py Normal file
View File

@@ -0,0 +1,107 @@
'''Stubs for tornado HTTP clients'''
from __future__ import absolute_import
import functools
from six import BytesIO
from tornado import httputil
from tornado.httpclient import HTTPResponse
from vcr.errors import CannotOverwriteExistingCassetteException
from vcr.request import Request
def vcr_fetch_impl(cassette, real_fetch_impl):
@functools.wraps(real_fetch_impl)
def new_fetch_impl(self, request, callback):
headers = request.headers.copy()
if request.user_agent:
headers.setdefault('User-Agent', request.user_agent)
# TODO body_producer, header_callback, and streaming_callback are not
# yet supported.
unsupported_call = (
getattr(request, 'body_producer', None) is not None or
request.header_callback is not None or
request.streaming_callback is not None
)
if unsupported_call:
response = HTTPResponse(
request,
599,
error=Exception(
"The request (%s) uses AsyncHTTPClient functionality "
"that is not yet supported by VCR.py. Please make the "
"request outside a VCR.py context." % repr(request)
),
request_time=self.io_loop.time() - request.start_time,
)
return callback(response)
vcr_request = Request(
request.method,
request.url,
request.body,
headers,
)
if cassette.can_play_response_for(vcr_request):
vcr_response = cassette.play_response(vcr_request)
headers = httputil.HTTPHeaders()
recorded_headers = vcr_response['headers']
if isinstance(recorded_headers, dict):
recorded_headers = recorded_headers.items()
for k, vs in recorded_headers:
for v in vs:
headers.add(k, v)
response = HTTPResponse(
request,
code=vcr_response['status']['code'],
reason=vcr_response['status']['message'],
headers=headers,
buffer=BytesIO(vcr_response['body']['string']),
effective_url=vcr_response.get('url'),
request_time=self.io_loop.time() - request.start_time,
)
return callback(response)
else:
if cassette.write_protected and cassette.filter_request(
vcr_request
):
response = HTTPResponse(
request,
599,
error=CannotOverwriteExistingCassetteException(
"No match for the request (%r) was found. "
"Can't overwrite existing cassette (%r) in "
"your current record mode (%r)."
% (vcr_request, cassette._path, cassette.record_mode)
),
request_time=self.io_loop.time() - request.start_time,
)
return callback(response)
def new_callback(response):
headers = [
(k, response.headers.get_list(k))
for k in response.headers.keys()
]
vcr_response = {
'status': {
'code': response.code,
'message': response.reason,
},
'headers': headers,
'body': {'string': response.body},
'url': response.effective_url,
}
cassette.append(vcr_request, vcr_response)
return callback(response)
real_fetch_impl(self, request, new_callback)
return new_fetch_impl

View File

@@ -1,8 +1,15 @@
'''Stubs for urllib3'''
from urllib3.connectionpool import VerifiedHTTPSConnection
from ..stubs import VCRHTTPSConnection
from urllib3.connectionpool import HTTPConnection, VerifiedHTTPSConnection
from ..stubs import VCRHTTPConnection, VCRHTTPSConnection
# urllib3 defines its own HTTPConnection classes. It includes some polyfills
# for newer features missing in older pythons.
class VCRVerifiedHTTPSConnection(VCRHTTPSConnection, VerifiedHTTPSConnection):
class VCRRequestsHTTPConnection(VCRHTTPConnection, HTTPConnection):
_baseclass = HTTPConnection
class VCRRequestsHTTPSConnection(VCRHTTPSConnection, VerifiedHTTPSConnection):
_baseclass = VerifiedHTTPSConnection

124
vcr/util.py Normal file
View File

@@ -0,0 +1,124 @@
import collections
import types
# Shamelessly stolen from https://github.com/kennethreitz/requests/blob/master/requests/structures.py
class CaseInsensitiveDict(collections.MutableMapping):
"""
A case-insensitive ``dict``-like object.
Implements all methods and operations of
``collections.MutableMapping`` as well as dict's ``copy``. Also
provides ``lower_items``.
All keys are expected to be strings. The structure remembers the
case of the last key to be set, and ``iter(instance)``,
``keys()``, ``items()``, ``iterkeys()``, and ``iteritems()``
will contain case-sensitive keys. However, querying and contains
testing is case insensitive::
cid = CaseInsensitiveDict()
cid['Accept'] = 'application/json'
cid['aCCEPT'] == 'application/json' # True
list(cid) == ['Accept'] # True
For example, ``headers['content-encoding']`` will return the
value of a ``'Content-Encoding'`` response header, regardless
of how the header name was originally stored.
If the constructor, ``.update``, or equality comparison
operations are given keys that have equal ``.lower()``s, the
behavior is undefined.
"""
def __init__(self, data=None, **kwargs):
self._store = dict()
if data is None:
data = {}
self.update(data, **kwargs)
def __setitem__(self, key, value):
# Use the lowercased key for lookups, but store the actual
# key alongside the value.
self._store[key.lower()] = (key, value)
def __getitem__(self, key):
return self._store[key.lower()][1]
def __delitem__(self, key):
del self._store[key.lower()]
def __iter__(self):
return (casedkey for casedkey, mappedvalue in self._store.values())
def __len__(self):
return len(self._store)
def lower_items(self):
"""Like iteritems(), but with all lowercase keys."""
return (
(lowerkey, keyval[1])
for (lowerkey, keyval)
in self._store.items()
)
def __eq__(self, other):
if isinstance(other, collections.Mapping):
other = CaseInsensitiveDict(other)
else:
return NotImplemented
# Compare insensitively
return dict(self.lower_items()) == dict(other.lower_items())
# Copy is required
def copy(self):
return CaseInsensitiveDict(self._store.values())
def __repr__(self):
return str(dict(self.items()))
def partition_dict(predicate, dictionary):
true_dict = {}
false_dict = {}
for key, value in dictionary.items():
this_dict = true_dict if predicate(key, value) else false_dict
this_dict[key] = value
return true_dict, false_dict
def compose(*functions):
def composed(incoming):
res = incoming
for function in reversed(functions):
if function:
res = function(res)
return res
return composed
def read_body(request):
if hasattr(request.body, 'read'):
return request.body.read()
return request.body
def auto_decorate(
decorator,
predicate=lambda name, value: isinstance(value, types.FunctionType)
):
def maybe_decorate(attribute, value):
if predicate(attribute, value):
value = decorator(value)
return value
class DecorateAll(type):
def __setattr__(cls, attribute, value):
return super(DecorateAll, cls).__setattr__(
attribute, maybe_decorate(attribute, value)
)
def __new__(cls, name, bases, attributes_dict):
new_attributes_dict = dict(
(attribute, maybe_decorate(attribute, value))
for attribute, value in attributes_dict.items()
)
return super(DecorateAll, cls).__new__(
cls, name, bases, new_attributes_dict
)
return DecorateAll