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

Compare commits

...

162 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
43 changed files with 800 additions and 296 deletions

2
.gitignore vendored
View File

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

View File

@@ -7,55 +7,62 @@ env:
- secure: LBSEg/gMj4u4Hrpo3zs6Y/1mTpd2RtcN49mZIFgTdbJ9IhpiNPqcEt647Lz94F9Eses2x2WbNuKqZKZZReY7QLbEzU1m0nN5jlaKrjcG5NR5clNABfFFyhgc0jBikyS4abAG8jc2efeaTrFuQwdoF4sE8YiVrkiVj2X5Xoi6sBk=
matrix:
- TOX_SUFFIX="flakes"
- TOX_SUFFIX="requests22"
- TOX_SUFFIX="requests23"
- TOX_SUFFIX="requests24"
- TOX_SUFFIX="requests25"
- TOX_SUFFIX="requests26"
- TOX_SUFFIX="requests27"
- TOX_SUFFIX="requests1"
- TOX_SUFFIX="httplib2"
- TOX_SUFFIX="boto"
- TOX_SUFFIX="boto3"
- TOX_SUFFIX="urllib317"
- TOX_SUFFIX="urllib319"
- TOX_SUFFIX="urllib3110"
- TOX_SUFFIX="tornado3"
- TOX_SUFFIX="urllib3121"
- TOX_SUFFIX="tornado4"
- TOX_SUFFIX="aiohttp"
matrix:
allow_failures:
- env: TOX_SUFFIX="boto"
- env: TOX_SUFFIX="boto3"
exclude:
include:
- env: TOX_SUFFIX="flakes"
python: 2.6
- env: TOX_SUFFIX="boto"
python: 3.3
- env: TOX_SUFFIX="boto"
python: 3.4
- env: TOX_SUFFIX="requests1"
python: 3.4
- env: TOX_SUFFIX="requests1"
python: 3.5
python: 3.7
dist: xenial
sudo: true
- env: TOX_SUFFIX="requests27"
python: 3.7
dist: xenial
sudo: true
- env: TOX_SUFFIX="httplib2"
python: 3.7
dist: xenial
sudo: true
- env: TOX_SUFFIX="urllib3121"
python: 3.7
dist: xenial
sudo: true
- env: TOX_SUFFIX="tornado4"
python: 3.7
dist: xenial
sudo: true
- env: TOX_SUFFIX="aiohttp"
python: 2.6
python: 3.7
dist: xenial
sudo: true
allow_failures:
- env: TOX_SUFFIX="boto3"
- env: TOX_SUFFIX="aiohttp"
python: "pypy3.5-5.9.0"
exclude:
# Only run flakes on a single Python 2.x and a single 3.x
- env: TOX_SUFFIX="flakes"
python: 3.4
- env: TOX_SUFFIX="flakes"
python: 3.5
- env: TOX_SUFFIX="flakes"
python: pypy
- env: TOX_SUFFIX="flakes"
python: "pypy3.5-5.9.0"
- env: TOX_SUFFIX="aiohttp"
python: 2.7
- env: TOX_SUFFIX="aiohttp"
python: 3.3
- env: TOX_SUFFIX="aiohttp"
python: pypy
- env: TOX_SUFFIX="aiohttp"
python: pypy3
python:
- 2.6
- 2.7
- 3.3
- 3.4
- 3.5
- 3.6
- pypy
- pypy3
- "pypy3.5-5.9.0"
install:
- pip install tox-travis
- if [[ $TOX_SUFFIX != 'flakes' ]]; then python setup.py install ; fi

View File

@@ -1,4 +1,4 @@
|PyPI| |Build Status| |Waffle Ready| |Gitter|
|PyPI| |Python versions| |Build Status| |Waffle Ready| |Gitter|
VCR.py
======
@@ -41,20 +41,6 @@ VCR.py will detect the absence of a cassette file and once again record
all HTTP interactions, which will update them to correspond to the new
API.
Support
-------
VCR.py works great with the following HTTP clients:
- requests
- aiohttp
- urllib3
- tornado
- urllib2
- boto
- boto3
License
=======
@@ -62,10 +48,12 @@ This library uses the MIT license. See `LICENSE.txt <LICENSE.txt>`__ for
more details
.. |PyPI| image:: https://img.shields.io/pypi/v/vcrpy.svg
:target: https://pypi.python.org/pypi/vcrpy-unittest
.. |Build Status| image:: https://secure.travis-ci.org/kevin1024/vcrpy.png?branch=master
:target: https://pypi.python.org/pypi/vcrpy
.. |Python versions| image:: https://img.shields.io/pypi/pyversions/vcrpy.svg
:target: https://pypi.python.org/pypi/vcrpy
.. |Build Status| image:: https://secure.travis-ci.org/kevin1024/vcrpy.svg?branch=master
:target: http://travis-ci.org/kevin1024/vcrpy
.. |Waffle Ready| image:: https://badge.waffle.io/kevin1024/vcrpy.png?label=ready&title=waffle
.. |Waffle Ready| image:: https://badge.waffle.io/kevin1024/vcrpy.svg?label=ready&title=waffle
:target: https://waffle.io/kevin1024/vcrpy
.. |Gitter| image:: https://badges.gitter.im/Join%20Chat.svg
:alt: Join the chat at https://gitter.im/kevin1024/vcrpy

View File

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

View File

@@ -1,5 +1,27 @@
Changelog
---------
- 2.0.0 - Support python 3.7 (fix httplib2 and urllib2, thanks @felixonmars)
[#356] Fixes `before_record_response` so the original response isn't changed (thanks @kgraves)
Fix requests stub when using proxy (thanks @samuelfekete @daneoshiga)
(only for aiohttp stub) Drop support to python 3.4 asyncio.coroutine (aiohttp doesn't support python it anymore)
Fix aiohttp stub to work with aiohttp client (thanks @stj)
Fix aiohttp stub to accept content type passed
Improve docs (thanks @adamchainz)
- 1.13.0 - Fix support to latest aiohttp version (3.3.2). Fix content-type bug in aiohttp stub. Save URL with query params properly when using aiohttp.
- 1.12.0 - Fix support to latest aiohttp version (3.2.1), Adapted setup to PEP508, Support binary responses on aiohttp, Dropped support for EOL python versions (2.6 and 3.3)
- 1.11.1 Fix compatibility with newest requests and urllib3 releases
- 1.11.0 Allow injection of persistence methods + bugfixes (thanks @j-funk and @IvanMalison),
Support python 3.6 + CI tests (thanks @derekbekoe and @graingert),
Support pytest-asyncio coroutines (thanks @graingert)
- 1.10.5 Added a fix to httplib2 (thanks @carlosds730), Fix an issue with
aiohttp (thanks @madninja), Add missing requirement yarl (thanks @lamenezes),
Remove duplicate mock triple (thanks @FooBarQuaxx)
- 1.10.4 Fix an issue with asyncio aiohttp (thanks @madninja)
- 1.10.3 Fix some issues with asyncio and params (thanks @anovikov1984 and
@lamenezes), Fix some issues with cassette serialize / deserialize and empty
response bodies (thanks @gRoussac and @dz0ny)
- 1.10.2 Fix 1.10.1 release - add aiohttp support back in
- 1.10.1 [bad release] Fix build for Fedora package + python2 (thanks @puiterwijk and @lamenezes)
- 1.10.0 Add support for aiohttp (thanks @lamenezes)
- 1.9.0 Add support for boto3 (thanks @desdm, @foorbarna). Fix deepcopy issue
for response headers when `decode_compressed_response` is enabled (thanks

View File

@@ -9,18 +9,20 @@ with pip::
Compatibility
-------------
VCR.py supports Python 2.6 and 2.7, 3.3, 3.4, and
VCR.py supports Python 2.7 and 3.4+, and
`pypy <http://pypy.org>`__.
The following http libraries are supported:
The following HTTP libraries are supported:
- urllib2
- urllib3
- http.client (python3)
- requests (both 1.x and 2.x versions)
- httplib2
- boto
- Tornado's AsyncHTTPClient
- ``aiohttp``
- ``boto``
- ``boto3``
- ``http.client``
- ``httplib2``
- ``requests`` (both 1.x and 2.x versions)
- ``tornado.httpclient``
- ``urllib2``
- ``urllib3``
Speed
-----
@@ -40,7 +42,7 @@ rebuilding pyyaml.
brew install libyaml # Mac with Homebrew
apt-get install libyaml-dev # Ubuntu
dnf install libyaml-dev # Fedora
dnf install libyaml-devel # Fedora
3. Rebuild pyyaml with libyaml::

View File

@@ -57,7 +57,7 @@ once
file.
It is similar to the new\_episodes record mode, but will prevent new,
unexpected requests from being made (i.e. because the request URI
unexpected requests from being made (e.g. because the request URI
changed).
once is the default record mode, used when you do not set one.

View File

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

View File

@@ -1,11 +1,9 @@
#!/usr/bin/env python
import sys
import logging
from setuptools import setup, find_packages
from setuptools.command.test import test as TestCommand
import pkg_resources
long_description = open('README.rst', 'r').read()
@@ -24,34 +22,22 @@ class PyTest(TestCommand):
sys.exit(errno)
install_requires = ['PyYAML', 'wrapt', 'six>=1.5']
extras_require = {
':python_version in "2.4, 2.5, 2.6"':
['contextlib2', 'backport_collections', 'mock'],
':python_version in "2.7, 3.1, 3.2"': ['contextlib2', 'mock'],
}
try:
if 'bdist_wheel' not in sys.argv:
for key, value in extras_require.items():
if key.startswith(':') and pkg_resources.evaluate_marker(key[1:]):
install_requires.extend(value)
except Exception:
logging.getLogger(__name__).exception(
'Something went wrong calculating platform specific dependencies, so '
"you're getting them all!"
)
for key, value in extras_require.items():
if key.startswith(':'):
install_requires.extend(value)
install_requires = [
'PyYAML',
'wrapt',
'six>=1.5',
'contextlib2; python_version=="2.7"',
'mock; python_version=="2.7"',
'yarl; python_version>="3.4"',
]
excluded_packages = ["tests*"]
if sys.version_info[0] == 2:
excluded_packages.append("vcr.stubs.aiohttp_stubs")
setup(
name='vcrpy',
version='1.10.0',
version='2.0.0',
description=(
"Automatically mock your HTTP interactions to simplify and "
"speed up testing"
@@ -60,9 +46,9 @@ setup(
author='Kevin McCarthy',
author_email='me@kevinmccarthy.org',
url='https://github.com/kevin1024/vcrpy',
packages=find_packages(exclude=("tests*",)),
packages=find_packages(exclude=excluded_packages),
python_requires='>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*',
install_requires=install_requires,
extras_require=extras_require,
license='MIT',
tests_require=['pytest', 'mock', 'pytest-httpbin'],
classifiers=[
@@ -70,7 +56,15 @@ setup(
'Environment :: Console',
'Intended Audience :: Developers',
'Programming Language :: Python',
'Programming Language :: Python :: 2',
'Programming Language :: Python :: 2.7',
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.4',
'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: 3.6',
'Programming Language :: Python :: 3.7',
'Programming Language :: Python :: Implementation :: CPython',
'Programming Language :: Python :: Implementation :: PyPy',
'Topic :: Software Development :: Testing',
'Topic :: Internet :: WWW/HTTP',
'License :: OSI Approved :: MIT License',

View File

@@ -1,7 +1,33 @@
# flake8: noqa
import asyncio
import aiohttp
from aiohttp.test_utils import TestClient
@asyncio.coroutine
def aiohttp_request(session, method, url, as_text, **kwargs):
response = yield from session.request(method, url, **kwargs) # NOQA: E999
return response, (yield from response.text()) if as_text else (yield from response.json()) # NOQA: E999
async def aiohttp_request(loop, method, url, output='text', encoding='utf-8', content_type=None, **kwargs):
session = aiohttp.ClientSession(loop=loop)
response_ctx = session.request(method, url, **kwargs)
response = await response_ctx.__aenter__()
if output == 'text':
content = await response.text()
elif output == 'json':
content_type = content_type or 'application/json'
content = await response.json(encoding=encoding, content_type=content_type)
elif output == 'raw':
content = await response.read()
response_ctx._resp.close()
await session.close()
return response, content
def aiohttp_app():
async def hello(request):
return aiohttp.web.Response(text='hello')
app = aiohttp.web.Application()
app.router.add_get('/', hello)
return app

View File

@@ -1,28 +1,33 @@
import contextlib
import pytest
asyncio = pytest.importorskip("asyncio")
aiohttp = pytest.importorskip("aiohttp")
import asyncio # NOQA
import sys # NOQA
import aiohttp # NOQA
import pytest # NOQA
import vcr # NOQA
from .aiohttp_utils import aiohttp_request # NOQA
import vcr # noqa: E402
from .aiohttp_utils import aiohttp_app, aiohttp_request # noqa: E402
def get(url, as_text=True, **kwargs):
loop = asyncio.get_event_loop()
with aiohttp.ClientSession() as session:
task = loop.create_task(aiohttp_request(session, 'GET', url, as_text, **kwargs))
def run_in_loop(fn):
with contextlib.closing(asyncio.new_event_loop()) as loop:
asyncio.set_event_loop(loop)
task = loop.create_task(fn(loop))
return loop.run_until_complete(task)
def post(url, as_text=True, **kwargs):
loop = asyncio.get_event_loop()
with aiohttp.ClientSession() as session:
task = loop.create_task(aiohttp_request(session, 'POST', url, as_text, **kwargs))
return loop.run_until_complete(task)
def request(method, url, output='text', **kwargs):
def run(loop):
return aiohttp_request(loop, method, url, output=output, **kwargs)
return run_in_loop(run)
def get(url, output='text', **kwargs):
return request('GET', url, output=output, **kwargs)
def post(url, output='text', **kwargs):
return request('POST', url, output='text', **kwargs)
@pytest.fixture(params=["https", "http"])
@@ -66,15 +71,28 @@ def test_text(tmpdir, scheme):
def test_json(tmpdir, scheme):
url = scheme + '://httpbin.org/get'
headers = {'Content-Type': 'application/json'}
with vcr.use_cassette(str(tmpdir.join('json.yaml'))):
_, response_json = get(url, as_text=False)
_, response_json = get(url, output='json', headers=headers)
with vcr.use_cassette(str(tmpdir.join('json.yaml'))) as cassette:
_, cassette_response_json = get(url, as_text=False)
_, cassette_response_json = get(url, output='json', headers=headers)
assert cassette_response_json == response_json
assert cassette.play_count == 1
def test_binary(tmpdir, scheme):
url = scheme + '://httpbin.org/image/png'
with vcr.use_cassette(str(tmpdir.join('binary.yaml'))):
_, response_binary = get(url, output='raw')
with vcr.use_cassette(str(tmpdir.join('binary.yaml'))) as cassette:
_, cassette_response_binary = get(url, output='raw')
assert cassette_response_binary == response_binary
assert cassette.play_count == 1
def test_post(tmpdir, scheme):
data = {'key1': 'value1', 'key2': 'value2'}
url = scheme + '://httpbin.org/post'
@@ -85,3 +103,77 @@ def test_post(tmpdir, scheme):
_, cassette_response_json = post(url, data=data)
assert cassette_response_json == response_json
assert cassette.play_count == 1
def test_params(tmpdir, scheme):
url = scheme + '://httpbin.org/get'
headers = {'Content-Type': 'application/json'}
params = {'a': 1, 'b': False, 'c': 'c'}
with vcr.use_cassette(str(tmpdir.join('get.yaml'))) as cassette:
_, response_json = get(url, output='json', params=params, headers=headers)
with vcr.use_cassette(str(tmpdir.join('get.yaml'))) as cassette:
_, cassette_response_json = get(url, output='json', params=params, headers=headers)
assert cassette_response_json == response_json
assert cassette.play_count == 1
def test_params_same_url_distinct_params(tmpdir, scheme):
url = scheme + '://httpbin.org/get'
headers = {'Content-Type': 'application/json'}
params = {'a': 1, 'b': False, 'c': 'c'}
with vcr.use_cassette(str(tmpdir.join('get.yaml'))) as cassette:
_, response_json = get(url, output='json', params=params, headers=headers)
with vcr.use_cassette(str(tmpdir.join('get.yaml'))) as cassette:
_, cassette_response_json = get(url, output='json', params=params, headers=headers)
assert cassette_response_json == response_json
assert cassette.play_count == 1
other_params = {'other': 'params'}
with vcr.use_cassette(str(tmpdir.join('get.yaml'))) as cassette:
response, cassette_response_text = get(url, output='text', params=other_params)
assert 'No match for the request' in cassette_response_text
assert response.status == 599
def test_params_on_url(tmpdir, scheme):
url = scheme + '://httpbin.org/get?a=1&b=foo'
headers = {'Content-Type': 'application/json'}
with vcr.use_cassette(str(tmpdir.join('get.yaml'))) as cassette:
_, response_json = get(url, output='json', headers=headers)
request = cassette.requests[0]
assert request.url == url
with vcr.use_cassette(str(tmpdir.join('get.yaml'))) as cassette:
_, cassette_response_json = get(url, output='json', headers=headers)
request = cassette.requests[0]
assert request.url == url
assert cassette_response_json == response_json
assert cassette.play_count == 1
def test_aiohttp_test_client(aiohttp_client, tmpdir):
loop = asyncio.get_event_loop()
app = aiohttp_app()
url = '/'
client = loop.run_until_complete(aiohttp_client(app))
with vcr.use_cassette(str(tmpdir.join('get.yaml'))):
response = loop.run_until_complete(client.get(url))
assert response.status == 200
response_text = loop.run_until_complete(response.text())
assert response_text == 'hello'
with vcr.use_cassette(str(tmpdir.join('get.yaml'))) as cassette:
response = loop.run_until_complete(client.get(url))
request = cassette.requests[0]
assert request.url == str(client.make_url(url))
response_text = loop.run_until_complete(response.text())
assert response_text == 'hello'
assert cassette.play_count == 1

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,12 +1,12 @@
# -*- coding: utf-8 -*-
'''Integration tests with httplib2'''
# External imports
import sys
from six.moves.urllib_parse import urlencode
import pytest
import pytest_httpbin.certs
# Internal imports
import vcr
from assertions import assert_cassette_has_one_response
@@ -19,7 +19,12 @@ def http():
Returns an httplib2 HTTP instance
with the certificate replaced by the httpbin one.
"""
return httplib2.Http(ca_certs=pytest_httpbin.certs.where())
kwargs = {
'ca_certs': pytest_httpbin.certs.where()
}
if sys.version_info[:2] == (3, 7):
kwargs['disable_ssl_certificate_validation'] = True
return httplib2.Http(**kwargs)
def test_response_code(tmpdir, httpbin_both):

View File

@@ -26,9 +26,9 @@ def test_ignore_localhost(tmpdir, httpbin):
with overridden_dns({'httpbin.org': '127.0.0.1'}):
cass_file = str(tmpdir.join('filter_qs.yaml'))
with vcr.use_cassette(cass_file, ignore_localhost=True) as cass:
urlopen('http://localhost:{0}/'.format(httpbin.port))
urlopen('http://localhost:{}/'.format(httpbin.port))
assert len(cass) == 0
urlopen('http://httpbin.org:{0}/'.format(httpbin.port))
urlopen('http://httpbin.org:{}/'.format(httpbin.port))
assert len(cass) == 1
@@ -39,9 +39,9 @@ def test_ignore_httpbin(tmpdir, httpbin):
cass_file,
ignore_hosts=['httpbin.org']
) as cass:
urlopen('http://httpbin.org:{0}/'.format(httpbin.port))
urlopen('http://httpbin.org:{}/'.format(httpbin.port))
assert len(cass) == 0
urlopen('http://localhost:{0}/'.format(httpbin.port))
urlopen('http://localhost:{}/'.format(httpbin.port))
assert len(cass) == 1
@@ -53,8 +53,8 @@ def test_ignore_localhost_and_httpbin(tmpdir, httpbin):
ignore_hosts=['httpbin.org'],
ignore_localhost=True
) as cass:
urlopen('http://httpbin.org:{0}'.format(httpbin.port))
urlopen('http://localhost:{0}'.format(httpbin.port))
urlopen('http://httpbin.org:{}'.format(httpbin.port))
urlopen('http://localhost:{}'.format(httpbin.port))
assert len(cass) == 0
@@ -62,12 +62,12 @@ def test_ignore_localhost_twice(tmpdir, httpbin):
with overridden_dns({'httpbin.org': '127.0.0.1'}):
cass_file = str(tmpdir.join('filter_qs.yaml'))
with vcr.use_cassette(cass_file, ignore_localhost=True) as cass:
urlopen('http://localhost:{0}'.format(httpbin.port))
urlopen('http://localhost:{}'.format(httpbin.port))
assert len(cass) == 0
urlopen('http://httpbin.org:{0}'.format(httpbin.port))
urlopen('http://httpbin.org:{}'.format(httpbin.port))
assert len(cass) == 1
with vcr.use_cassette(cass_file, ignore_localhost=True) as cass:
assert len(cass) == 1
urlopen('http://localhost:{0}'.format(httpbin.port))
urlopen('http://httpbin.org:{0}'.format(httpbin.port))
urlopen('http://localhost:{}'.format(httpbin.port))
urlopen('http://httpbin.org:{}'.format(httpbin.port))
assert len(cass) == 1

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

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

@@ -1,11 +1,13 @@
# -*- coding: utf-8 -*-
'''Test requests' interaction with vcr'''
import platform
import pytest
import sys
import vcr
from assertions import assert_cassette_empty, assert_is_json
requests = pytest.importorskip("requests")
from requests.exceptions import ConnectionError # noqa E402
def test_status_code(httpbin_both, tmpdir):
@@ -38,6 +40,18 @@ def test_body(tmpdir, httpbin_both):
assert content == requests.get(url).content
def test_get_empty_content_type_json(tmpdir, httpbin_both):
'''Ensure GET with application/json content-type and empty request body doesn't crash'''
url = httpbin_both + '/status/200'
headers = {'Content-Type': 'application/json'}
with vcr.use_cassette(str(tmpdir.join('get_empty_json.yaml')), match_on=('body',)):
status = requests.get(url, headers=headers).status_code
with vcr.use_cassette(str(tmpdir.join('get_empty_json.yaml')), match_on=('body',)):
assert status == requests.get(url, headers=headers).status_code
def test_effective_url(tmpdir, httpbin_both):
'''Ensure that the effective_url is captured'''
url = httpbin_both.url + '/redirect-to?url=/html'
@@ -88,11 +102,29 @@ def test_post(tmpdir, httpbin_both):
assert req1 == req2
def test_post_chunked_binary(tmpdir, httpbin_both):
def test_post_chunked_binary(tmpdir, httpbin):
'''Ensure that we can send chunked binary without breaking while trying to concatenate bytes with str.'''
data1 = iter([b'data', b'to', b'send'])
data2 = iter([b'data', b'to', b'send'])
url = httpbin_both.url + '/post'
url = httpbin.url + '/post'
with vcr.use_cassette(str(tmpdir.join('requests.yaml'))):
req1 = requests.post(url, data1).content
with vcr.use_cassette(str(tmpdir.join('requests.yaml'))):
req2 = requests.post(url, data2).content
assert req1 == req2
@pytest.mark.xskip('sys.version_info >= (3, 6)', strict=True, raises=ConnectionError)
@pytest.mark.xskip((3, 5) < sys.version_info < (3, 6) and
platform.python_implementation() == 'CPython',
reason='Fails on CPython 3.5')
def test_post_chunked_binary_secure(tmpdir, httpbin_secure):
'''Ensure that we can send chunked binary without breaking while trying to concatenate bytes with str.'''
data1 = iter([b'data', b'to', b'send'])
data2 = iter([b'data', b'to', b'send'])
url = httpbin_secure.url + '/post'
with vcr.use_cassette(str(tmpdir.join('requests.yaml'))):
req1 = requests.post(url, data1).content
print(req1)
@@ -222,10 +254,8 @@ def test_nested_cassettes_with_session_created_before_nesting(httpbin_both, tmpd
def test_post_file(tmpdir, httpbin_both):
'''Ensure that we handle posting a file.'''
url = httpbin_both + '/post'
with vcr.use_cassette(str(tmpdir.join('post_file.yaml'))) as cass:
# Don't use 2.7+ only style ',' separated with here because we support python 2.6
with open('tox.ini') as f:
original_response = requests.post(url, f).content
with vcr.use_cassette(str(tmpdir.join('post_file.yaml'))) as cass, open('tox.ini') as f:
original_response = requests.post(url, f).content
# This also tests that we do the right thing with matching the body when they are files.
with vcr.use_cassette(str(tmpdir.join('post_file.yaml')),

View File

@@ -1,5 +1,6 @@
import vcr
import zlib
import json
import six.moves.http_client as httplib
from assertions import assert_is_json
@@ -83,3 +84,50 @@ def test_original_decoded_response_is_not_modified(tmpdir, httpbin):
assert 'content-encoding' not in inside.headers
assert_is_json(inside.read())
def _make_before_record_response(fields, replacement='[REDACTED]'):
def before_record_response(response):
string_body = response['body']['string'].decode('utf8')
body = json.loads(string_body)
for field in fields:
if field in body:
body[field] = replacement
response['body']['string'] = json.dumps(body).encode()
return response
return before_record_response
def test_original_response_is_not_modified_by_before_filter(tmpdir, httpbin):
testfile = str(tmpdir.join('sensitive_data_scrubbed_response.yml'))
host, port = httpbin.host, httpbin.port
field_to_scrub = 'url'
replacement = '[YOU_CANT_HAVE_THE_MANGO]'
conn = httplib.HTTPConnection(host, port)
conn.request('GET', '/get')
outside = conn.getresponse()
callback = _make_before_record_response([field_to_scrub], replacement)
with vcr.use_cassette(testfile, before_record_response=callback):
conn = httplib.HTTPConnection(host, port)
conn.request('GET', '/get')
inside = conn.getresponse()
# The scrubbed field should be the same, because no cassette existed.
# Furthermore, the responses should be identical.
inside_body = json.loads(inside.read().decode('utf-8'))
outside_body = json.loads(outside.read().decode('utf-8'))
assert not inside_body[field_to_scrub] == replacement
assert inside_body[field_to_scrub] == outside_body[field_to_scrub]
# Ensure that when a cassette exists, the scrubbed response is returned.
with vcr.use_cassette(testfile, before_record_response=callback):
conn = httplib.HTTPConnection(host, port)
conn.request('GET', '/get')
inside = conn.getresponse()
inside_body = json.loads(inside.read().decode('utf-8'))
assert inside_body[field_to_scrub] == replacement

View File

@@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
'''Integration tests with urllib2'''
import ssl
from six.moves.urllib.request import urlopen
from six.moves.urllib_parse import urlencode
import pytest_httpbin.certs
@@ -12,7 +13,9 @@ from assertions import assert_cassette_has_one_response
def urlopen_with_cafile(*args, **kwargs):
kwargs['cafile'] = pytest_httpbin.certs.where()
context = ssl.create_default_context(cafile=pytest_httpbin.certs.where())
context.check_hostname = False
kwargs['context'] = context
try:
return urlopen(*args, **kwargs)
except TypeError:

View File

@@ -5,6 +5,7 @@
import pytest
import pytest_httpbin
import vcr
from vcr.patch import force_reset
from assertions import assert_cassette_empty, assert_is_json
urllib3 = pytest.importorskip("urllib3")
@@ -55,7 +56,7 @@ def test_body(tmpdir, httpbin_both, verify_pool_mgr):
def test_auth(tmpdir, httpbin_both, verify_pool_mgr):
'''Ensure that we can handle basic auth'''
auth = ('user', 'passwd')
headers = urllib3.util.make_headers(basic_auth='{0}:{1}'.format(*auth))
headers = urllib3.util.make_headers(basic_auth='{}:{}'.format(*auth))
url = httpbin_both.url + '/basic-auth/user/passwd'
with vcr.use_cassette(str(tmpdir.join('auth.yaml'))):
one = verify_pool_mgr.request('GET', url, headers=headers)
@@ -69,7 +70,7 @@ def test_auth(tmpdir, httpbin_both, verify_pool_mgr):
def test_auth_failed(tmpdir, httpbin_both, verify_pool_mgr):
'''Ensure that we can save failed auth statuses'''
auth = ('user', 'wrongwrongwrong')
headers = urllib3.util.make_headers(basic_auth='{0}:{1}'.format(*auth))
headers = urllib3.util.make_headers(basic_auth='{}:{}'.format(*auth))
url = httpbin_both.url + '/basic-auth/user/passwd'
with vcr.use_cassette(str(tmpdir.join('auth-failed.yaml'))) as cass:
# Ensure that this is empty to begin with
@@ -138,3 +139,21 @@ def test_gzip(tmpdir, httpbin_both, verify_pool_mgr):
def test_https_with_cert_validation_disabled(tmpdir, httpbin_secure, pool_mgr):
with vcr.use_cassette(str(tmpdir.join('cert_validation_disabled.yaml'))):
pool_mgr.request('GET', httpbin_secure.url)
def test_urllib3_force_reset():
cpool = urllib3.connectionpool
http_original = cpool.HTTPConnection
https_original = cpool.HTTPSConnection
verified_https_original = cpool.VerifiedHTTPSConnection
with vcr.use_cassette(path='test'):
first_cassette_HTTPConnection = cpool.HTTPConnection
first_cassette_HTTPSConnection = cpool.HTTPSConnection
first_cassette_VerifiedHTTPSConnection = cpool.VerifiedHTTPSConnection
with force_reset():
assert cpool.HTTPConnection is http_original
assert cpool.HTTPSConnection is https_original
assert cpool.VerifiedHTTPSConnection is verified_https_original
assert cpool.HTTPConnection is first_cassette_HTTPConnection
assert cpool.HTTPSConnection is first_cassette_HTTPSConnection
assert cpool.VerifiedHTTPSConnection is first_cassette_VerifiedHTTPSConnection

View File

@@ -1,5 +1,6 @@
import multiprocessing
import pytest
from six.moves import xmlrpc_client
from six.moves import xmlrpc_client, xmlrpc_server
requests = pytest.importorskip("requests")
@@ -80,13 +81,27 @@ def test_amazon_doctype(tmpdir):
assert 'html' in r.text
def test_xmlrpclib(tmpdir):
@pytest.yield_fixture(scope='session')
def rpc_server():
httpd = xmlrpc_server.SimpleXMLRPCServer(('', 0))
httpd.register_function(pow)
proxy_process = multiprocessing.Process(
target=httpd.serve_forever,
)
try:
proxy_process.start()
yield 'http://{}:{}'.format(*httpd.server_address)
finally:
proxy_process.terminate()
def test_xmlrpclib(tmpdir, rpc_server):
with vcr.use_cassette(str(tmpdir.join('xmlrpcvideo.yaml'))):
roundup_server = xmlrpc_client.ServerProxy('http://bugs.python.org/xmlrpc', allow_none=True)
original_schema = roundup_server.schema()
roundup_server = xmlrpc_client.ServerProxy(rpc_server, allow_none=True)
original_schema = roundup_server.pow(2, 4)
with vcr.use_cassette(str(tmpdir.join('xmlrpcvideo.yaml'))):
roundup_server = xmlrpc_client.ServerProxy('http://bugs.python.org/xmlrpc', allow_none=True)
second_schema = roundup_server.schema()
roundup_server = xmlrpc_client.ServerProxy(rpc_server, allow_none=True)
second_schema = roundup_server.pow(2, 4)
assert original_schema == second_schema

View File

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

View File

@@ -22,7 +22,7 @@ def assert_matcher(matcher_name):
matcher = getattr(matchers, matcher_name)
for k1, k2 in itertools.permutations(REQUESTS, 2):
matched = matcher(REQUESTS[k1], REQUESTS[k2])
if matcher_name in set((k1, k2)):
if matcher_name in {k1, k2}:
assert not matched
else:
assert matched
@@ -31,7 +31,7 @@ def assert_matcher(matcher_name):
def test_uri_matcher():
for k1, k2 in itertools.permutations(REQUESTS, 2):
matched = matchers.uri(REQUESTS[k1], REQUESTS[k2])
if set((k1, k2)) != set(('base', 'method')):
if {k1, k2} != {'base', 'method'}:
assert not matched
else:
assert matched

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

@@ -4,7 +4,7 @@ import pytest
from vcr.compat import mock
from vcr.request import Request
from vcr.serialize import deserialize, serialize
from vcr.serializers import yamlserializer, jsonserializer
from vcr.serializers import yamlserializer, jsonserializer, compat
def test_deserialize_old_yaml_cassette():
@@ -131,3 +131,9 @@ def test_serialize_binary_request():
)
except (UnicodeDecodeError, TypeError) as exc:
assert msg in str(exc)
def test_deserialize_no_body_string():
data = {'body': {'string': None}}
output = compat.convert_to_bytes(data)
assert data == output

View File

@@ -1,4 +1,6 @@
from vcr.stubs import VCRHTTPSConnection
from vcr.compat import mock
from vcr.cassette import Cassette
class TestVCRConnection(object):
@@ -7,3 +9,10 @@ class TestVCRConnection(object):
vcr_connection = VCRHTTPSConnection('www.examplehost.com')
vcr_connection.ssl_version = 'example_ssl_version'
assert vcr_connection.real_connection.ssl_version == 'example_ssl_version'
@mock.patch('vcr.cassette.Cassette.can_play_response_for', return_value=False)
def testing_connect(*args):
vcr_connection = VCRHTTPSConnection('www.google.com')
vcr_connection.cassette = Cassette('test', record_mode='all')
vcr_connection.real_connection.connect()
assert vcr_connection.real_connection.sock is not None

View File

@@ -94,7 +94,7 @@ def test_vcr_before_record_response_iterable():
response = object() # just can't be None
# Prevent actually saving the cassette
with mock.patch('vcr.cassette.save_cassette'):
with mock.patch('vcr.cassette.FilesystemPersister.save_cassette'):
# Baseline: non-iterable before_record_response should work
mock_filter = mock.Mock()
@@ -118,7 +118,7 @@ def test_before_record_response_as_filter():
response = object() # just can't be None
# Prevent actually saving the cassette
with mock.patch('vcr.cassette.save_cassette'):
with mock.patch('vcr.cassette.FilesystemPersister.save_cassette'):
filter_all = mock.Mock(return_value=None)
vcr = VCR(before_record_response=filter_all)
@@ -132,7 +132,7 @@ def test_vcr_path_transformer():
# Regression test for #199
# Prevent actually saving the cassette
with mock.patch('vcr.cassette.save_cassette'):
with mock.patch('vcr.cassette.FilesystemPersister.save_cassette'):
# Baseline: path should be unchanged
vcr = VCR()
@@ -319,11 +319,11 @@ def test_additional_matchers():
@vcr.use_cassette
def function_defaults(cassette):
assert set(cassette._match_on) == set([vcr.matchers['uri']])
assert set(cassette._match_on) == {vcr.matchers['uri']}
@vcr.use_cassette(additional_matchers=('body',))
def function_additional(cassette):
assert set(cassette._match_on) == set([vcr.matchers['uri'], vcr.matchers['body']])
assert set(cassette._match_on) == {vcr.matchers['uri'], vcr.matchers['body']}
function_defaults()
function_additional()

27
tox.ini
View File

@@ -1,5 +1,5 @@
[tox]
envlist = {py26,py27,py33,py34,pypy,pypy3}-{flakes,requests27,requests26,requests25,requests24,requests23,requests22,requests1,httplib2,urllib317,urllib319,urllib3110,tornado3,tornado4,boto,boto3,aiohttp}
envlist = {py27,py35,py36,py37,pypy}-{flakes,requests27,httplib2,urllib3121,tornado4,boto3,aiohttp}
[testenv:flakes]
skipsdist = True
@@ -13,32 +13,21 @@ deps = flake8
commands =
./runtests.sh {posargs}
deps =
# httpbin fails with latest Flask, so we pin it
Flask==0.10.1
Flask<1
mock
pytest
pytest-httpbin
PyYAML
requests1: requests==1.2.3
requests27: requests==2.7.0
requests26: requests==2.6.0
requests25: requests==2.5.0
requests24: requests==2.4.0
requests23: requests==2.3.0
requests22: requests==2.2.1
httplib2: httplib2
urllib317: urllib3==1.7.1
urllib319: urllib3==1.9.1
urllib3110: urllib3==1.10.2
{py26,py27,py33,py34,pypy}-tornado3: tornado>=3,<4
{py26,py27,py33,py34,pypy}-tornado4: tornado>=4,<5
{py26,py27,py33,py34,pypy}-tornado3: pytest-tornado
{py26,py27,py33,py34,pypy}-tornado4: pytest-tornado
{py26,py27,py33,py34}-tornado3: pycurl
{py26,py27,py33,py34}-tornado4: pycurl
boto: boto
urllib3121: urllib3==1.21.1
{py27,py35,py36,pypy}-tornado4: tornado>=4,<5
{py27,py35,py36,pypy}-tornado4: pytest-tornado
{py27,py35,py36}-tornado4: pycurl
boto3: boto3
aiohttp: aiohttp
aiohttp: pytest-asyncio
aiohttp: pytest-aiohttp
[flake8]
max_line_length = 110

3
vcr/_handle_coroutine.py Normal file
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,17 +1,29 @@
import collections
import copy
import sys
import inspect
import logging
import wrapt
from .compat import contextlib, collections
from .compat import contextlib
from .errors import UnhandledHTTPRequestError
from .matchers import requests_match, uri, method
from .patch import CassettePatcherBuilder
from .persist import load_cassette, save_cassette
from .serializers import yamlserializer
from .persisters.filesystem import FilesystemPersister
from .util import partition_dict
try:
from asyncio import iscoroutinefunction
from ._handle_coroutine import handle_coroutine
except ImportError:
def iscoroutinefunction(*args, **kwargs):
return False
def handle_coroutine(*args, **kwags):
raise NotImplementedError('Not implemented on Python 2')
log = logging.getLogger(__name__)
@@ -96,18 +108,25 @@ class CassetteContextDecorator(object):
)
def _execute_function(self, function, args, kwargs):
if inspect.isgeneratorfunction(function):
handler = self._handle_coroutine
else:
handler = self._handle_function
return handler(function, args, kwargs)
def handle_function(cassette):
if cassette.inject:
return function(cassette, *args, **kwargs)
else:
return function(*args, **kwargs)
def _handle_coroutine(self, function, args, kwargs):
"""Wraps a coroutine so that we're inside the cassette context for the
duration of the coroutine.
if iscoroutinefunction(function):
return handle_coroutine(vcr=self, fn=handle_function)
if inspect.isgeneratorfunction(function):
return self._handle_generator(fn=handle_function)
return self._handle_function(fn=handle_function)
def _handle_generator(self, fn):
"""Wraps a generator so that we're inside the cassette context for the
duration of the generator.
"""
with self as cassette:
coroutine = self.__handle_function(cassette, function, args, kwargs)
coroutine = fn(cassette)
# We don't need to catch StopIteration. The caller (Tornado's
# gen.coroutine, for example) will handle that.
to_yield = next(coroutine)
@@ -117,17 +136,14 @@ class CassetteContextDecorator(object):
except Exception:
to_yield = coroutine.throw(*sys.exc_info())
else:
to_yield = coroutine.send(to_send)
try:
to_yield = coroutine.send(to_send)
except StopIteration:
break
def __handle_function(self, cassette, function, args, kwargs):
if cassette.inject:
return function(cassette, *args, **kwargs)
else:
return function(*args, **kwargs)
def _handle_function(self, function, args, kwargs):
def _handle_function(self, fn):
with self as cassette:
return self.__handle_function(cassette, function, args, kwargs)
return fn(cassette)
@staticmethod
def get_function_name(function):
@@ -163,13 +179,13 @@ class Cassette(object):
def use(cls, **kwargs):
return CassetteContextDecorator.from_args(cls, **kwargs)
def __init__(self, path, serializer=yamlserializer, record_mode='once',
def __init__(self, path, serializer=None, persister=None, record_mode='once',
match_on=(uri, method), before_record_request=None,
before_record_response=None, custom_patches=(),
inject=False):
self._persister = persister or FilesystemPersister
self._path = path
self._serializer = serializer
self._serializer = serializer or yamlserializer
self._match_on = match_on
self._before_record_request = before_record_request or (lambda x: x)
self._before_record_response = before_record_response or (lambda x: x)
@@ -210,6 +226,9 @@ class Cassette(object):
request = self._before_record_request(request)
if not request:
return
# Deepcopy is here because mutation of `response` will corrupt the
# real response.
response = copy.deepcopy(response)
response = self._before_record_response(response)
if response is None:
return
@@ -271,28 +290,28 @@ class Cassette(object):
def _save(self, force=False):
if force or self.dirty:
save_cassette(
self._persister.save_cassette(
self._path,
self._as_dict(),
serializer=self._serializer
serializer=self._serializer,
)
self.dirty = False
def _load(self):
try:
requests, responses = load_cassette(
requests, responses = self._persister.load_cassette(
self._path,
serializer=self._serializer
serializer=self._serializer,
)
for request, response in zip(requests, responses):
self.append(request, response)
self.dirty = False
self.rewound = True
except IOError:
except ValueError:
pass
def __str__(self):
return "<Cassette containing {0} recorded response(s)>".format(
return "<Cassette containing {} recorded response(s)>".format(
len(self)
)

View File

@@ -11,8 +11,4 @@ else:
if not hasattr(contextlib, 'ExitStack'):
import contextlib2 as contextlib
import collections
if not hasattr(collections, 'Counter'):
import backport_collections as collections
__all__ = ['mock', 'contextlib', 'collections']
__all__ = ['mock', 'contextlib']

View File

@@ -1,4 +1,5 @@
import copy
import collections
import functools
import inspect
import os
@@ -6,9 +7,9 @@ import types
import six
from .compat import collections
from .cassette import Cassette
from .serializers import yamlserializer, jsonserializer
from .persisters.filesystem import FilesystemPersister
from .util import compose, auto_decorate
from . import matchers
from . import filters
@@ -57,6 +58,7 @@ class VCR(object):
'raw_body': matchers.raw_body,
'body': matchers.body,
}
self.persister = FilesystemPersister
self.record_mode = record_mode
self.filter_headers = filter_headers
self.filter_query_parameters = filter_query_parameters
@@ -76,7 +78,7 @@ class VCR(object):
serializer = self.serializers[serializer_name]
except KeyError:
raise KeyError(
"Serializer {0} doesn't exist or isn't registered".format(
"Serializer {} doesn't exist or isn't registered".format(
serializer_name
)
)
@@ -89,7 +91,7 @@ class VCR(object):
matchers.append(self.matchers[m])
except KeyError:
raise KeyError(
"Matcher {0} doesn't exist or isn't registered".format(m)
"Matcher {} doesn't exist or isn't registered".format(m)
)
return matchers
@@ -143,6 +145,7 @@ class VCR(object):
merged_config = {
'serializer': self._get_serializer(serializer_name),
'persister': self.persister,
'match_on': self._get_matchers(
tuple(matcher_names) + tuple(additional_matchers)
),
@@ -270,6 +273,10 @@ class VCR(object):
def register_matcher(self, name, matcher):
self.matchers[name] = matcher
def register_persister(self, persister):
# Singleton, no name required
self.persister = persister
def test_case(self, predicate=None):
predicate = predicate or self.is_test_method
return six.with_metaclass(auto_decorate(self.use_cassette, predicate))

View File

@@ -49,7 +49,8 @@ def _transform_json(body):
# Request body is always a byte string, but json.loads() wants a text
# string. RFC 7159 says the default encoding is UTF-8 (although UTF-16
# and UTF-32 are also allowed: hmmmmm).
return json.loads(body.decode('utf-8'))
if body:
return json.loads(body.decode('utf-8'))
_xml_header_checker = _header_checker('text/xml')
@@ -89,12 +90,12 @@ def _log_matches(r1, r2, matches):
differences = [m for m in matches if not m[0]]
if differences:
log.debug(
"Requests {0} and {1} differ according to "
"the following matchers: {2}".format(r1, r2, differences)
"Requests {} and {} differ according to "
"the following matchers: {}".format(r1, r2, differences)
)
def requests_match(r1, r2, matchers):
matches = [(m(r1, r2), m) for m in matchers]
_log_matches(r1, r2, matches)
return all([m[0] for m in matches])
return all(m[0] for m in matches)

View File

@@ -59,7 +59,7 @@ def build_uri(**parts):
port = parts['port']
scheme = parts['protocol']
default_port = {'https': 443, 'http': 80}[scheme]
parts['port'] = ':{0}'.format(port) if port != default_port else ''
parts['port'] = ':{}'.format(port) if port != default_port else ''
return "{protocol}://{host}{port}{path}".format(**parts)
@@ -161,8 +161,9 @@ def main():
for file_path in files:
migrated = try_migrate(file_path)
status = 'OK' if migrated else 'FAIL'
sys.stderr.write("[{0}] {1}\n".format(status, file_path))
sys.stderr.write("[{}] {}\n".format(status, file_path))
sys.stderr.write("Done.\n")
if __name__ == '__main__':
main()

View File

@@ -12,16 +12,6 @@ _HTTPConnection = httplib.HTTPConnection
_HTTPSConnection = httplib.HTTPSConnection
# Try to save the original types for requests
try:
import requests.packages.urllib3.connectionpool as cpool
except ImportError: # pragma: no cover
pass
else:
_VerifiedHTTPSConnection = cpool.VerifiedHTTPSConnection
_cpoolHTTPConnection = cpool.HTTPConnection
_cpoolHTTPSConnection = cpool.HTTPSConnection
# Try to save the original types for boto3
try:
import botocore.vendored.requests.packages.urllib3.connectionpool as cpool
@@ -32,14 +22,27 @@ else:
_cpoolBoto3HTTPConnection = cpool.HTTPConnection
_cpoolBoto3HTTPSConnection = cpool.HTTPSConnection
cpool = None
# Try to save the original types for urllib3
try:
import urllib3
import urllib3.connectionpool as cpool
except ImportError: # pragma: no cover
pass
else:
_VerifiedHTTPSConnection = urllib3.connectionpool.VerifiedHTTPSConnection
_VerifiedHTTPSConnection = cpool.VerifiedHTTPSConnection
_cpoolHTTPConnection = cpool.HTTPConnection
_cpoolHTTPSConnection = cpool.HTTPSConnection
# Try to save the original types for requests
try:
if not cpool:
import requests.packages.urllib3.connectionpool as cpool
except ImportError: # pragma: no cover
pass
else:
_VerifiedHTTPSConnection = cpool.VerifiedHTTPSConnection
_cpoolHTTPConnection = cpool.HTTPConnection
_cpoolHTTPSConnection = cpool.HTTPSConnection
# Try to save the original types for httplib2
@@ -166,7 +169,7 @@ class CassettePatcherBuilder(object):
bases = (base_class,)
if not issubclass(base_class, object): # Check for old style class
bases += (object,)
return type('{0}{1}'.format(base_class.__name__, self._cassette._path),
return type('{}{}'.format(base_class.__name__, self._cassette._path),
bases, dict(cassette=self._cassette))
@_build_patchers_from_mock_triples_decorator
@@ -176,10 +179,9 @@ class CassettePatcherBuilder(object):
def _requests(self):
try:
import requests.packages.urllib3.connectionpool as cpool
from .stubs import requests_stubs
except ImportError: # pragma: no cover
return ()
from .stubs import requests_stubs
return self._urllib3_patchers(cpool, requests_stubs)
def _boto3(self):
@@ -301,7 +303,6 @@ class CassettePatcherBuilder(object):
self._get_cassette_subclass(stubs.VCRRequestsHTTPSConnection)
)
mock_triples = (
(cpool, 'VerifiedHTTPSConnection', stubs.VCRRequestsHTTPSConnection),
(cpool, 'VerifiedHTTPSConnection', stubs.VCRRequestsHTTPSConnection),
(cpool, 'HTTPConnection', stubs.VCRRequestsHTTPConnection),
(cpool, 'HTTPSConnection', stubs.VCRRequestsHTTPSConnection),
@@ -361,8 +362,22 @@ class ConnectionRemover(object):
def reset_patchers():
yield mock.patch.object(httplib, 'HTTPConnection', _HTTPConnection)
yield mock.patch.object(httplib, 'HTTPSConnection', _HTTPSConnection)
try:
import requests.packages.urllib3.connectionpool as cpool
import requests
if requests.__build__ < 0x021603:
# Avoid double unmock if requests 2.16.3
# First, this is pointless, requests.packages.urllib3 *IS* urllib3 (see packages.py)
# Second, this is unmocking twice the same classes with different namespaces
# and is creating weird issues and bugs:
# > AssertionError: assert <class 'urllib3.connection.HTTPConnection'>
# > is <class 'requests.packages.urllib3.connection.HTTPConnection'>
# This assert should work!!!
# Note that this also means that now, requests.packages is never imported
# if requests 2.16.3 or greater is used with VCRPy.
import requests.packages.urllib3.connectionpool as cpool
else:
raise ImportError("Skip requests not vendored anymore")
except ImportError: # pragma: no cover
pass
else:
@@ -385,11 +400,11 @@ def reset_patchers():
pass
else:
yield mock.patch.object(cpool, 'VerifiedHTTPSConnection', _VerifiedHTTPSConnection)
yield mock.patch.object(cpool, 'HTTPConnection', _HTTPConnection)
yield mock.patch.object(cpool, 'HTTPSConnection', _HTTPSConnection)
yield mock.patch.object(cpool, 'HTTPConnection', _cpoolHTTPConnection)
yield mock.patch.object(cpool, 'HTTPSConnection', _cpoolHTTPSConnection)
if hasattr(cpool.HTTPConnectionPool, 'ConnectionCls'):
yield mock.patch.object(cpool.HTTPConnectionPool, 'ConnectionCls', _HTTPConnection)
yield mock.patch.object(cpool.HTTPSConnectionPool, 'ConnectionCls', _HTTPSConnection)
yield mock.patch.object(cpool.HTTPConnectionPool, 'ConnectionCls', _cpoolHTTPConnection)
yield mock.patch.object(cpool.HTTPSConnectionPool, 'ConnectionCls', _cpoolHTTPSConnection)
try:
import botocore.vendored.requests.packages.urllib3.connectionpool as cpool

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

@@ -81,7 +81,7 @@ class Request(object):
return self.scheme
def __str__(self):
return "<Request ({0}) {1}>".format(self.method, self.uri)
return "<Request ({}) {}>".format(self.method, self.uri)
def __repr__(self):
return self.__str__()

View File

@@ -24,7 +24,7 @@ def convert_body_to_bytes(resp):
http://pyyaml.org/wiki/PyYAMLDocumentation#Python3support
"""
try:
if not isinstance(resp['body']['string'], six.binary_type):
if resp['body']['string'] is not None and not isinstance(resp['body']['string'], six.binary_type):
resp['body']['string'] = resp['body']['string'].encode('utf-8')
except (KeyError, TypeError, UnicodeEncodeError):
# The thing we were converting either wasn't a dictionary or didn't

View File

@@ -18,7 +18,7 @@ log = logging.getLogger(__name__)
class VCRFakeSocket(object):
"""
A socket that doesn't do anything!
Used when playing back casssettes, when there
Used when playing back cassettes, when there
is no actual open socket.
"""
@@ -132,10 +132,13 @@ class VCRConnection(object):
"""
port = self.real_connection.port
default_port = {'https': 443, 'http': 80}[self._protocol]
return ':{0}'.format(port) if port != default_port else ''
return ':{}'.format(port) if port != default_port else ''
def _uri(self, url):
"""Returns request absolute URI"""
if url and not url.startswith('/'):
# Then this must be a proxy request.
return url
uri = "{0}://{1}{2}{3}".format(
self._protocol,
self.real_connection.host,
@@ -146,14 +149,14 @@ class VCRConnection(object):
def _url(self, uri):
"""Returns request selector url from absolute URI"""
prefix = "{0}://{1}{2}".format(
prefix = "{}://{}{}".format(
self._protocol,
self.real_connection.host,
self._port_postfix(),
)
return uri.replace(prefix, '', 1)
def request(self, method, url, body=None, headers=None):
def request(self, method, url, body=None, headers=None, *args, **kwargs):
'''Persist the request metadata in self._vcr_request'''
self._vcr_request = Request(
method=method,
@@ -161,13 +164,15 @@ class VCRConnection(object):
body=body,
headers=headers or {}
)
log.debug('Got {0}'.format(self._vcr_request))
log.debug('Got {}'.format(self._vcr_request))
# Note: The request may not actually be finished at this point, so
# I'm not sending the actual request until getresponse(). This
# allows me to compare the entire length of the response to see if it
# exists in the cassette.
self._sock = VCRFakeSocket()
def putrequest(self, method, url, *args, **kwargs):
"""
httplib gives you more than one way to do it. This is a way
@@ -180,7 +185,7 @@ class VCRConnection(object):
body="",
headers={}
)
log.debug('Got {0}'.format(self._vcr_request))
log.debug('Got {}'.format(self._vcr_request))
def putheader(self, header, *values):
self._vcr_request.headers[header] = values
@@ -214,7 +219,7 @@ class VCRConnection(object):
# then return it
if self.cassette.can_play_response_for(self._vcr_request):
log.info(
"Playing response for {0} from cassette".format(
"Playing response for {} from cassette".format(
self._vcr_request
)
)
@@ -236,7 +241,7 @@ class VCRConnection(object):
# and return it.
log.info(
"{0} not in cassette, sending to real server".format(
"{} not in cassette, sending to real server".format(
self._vcr_request
)
)
@@ -287,13 +292,17 @@ class VCRConnection(object):
# Cassette is write-protected, don't actually connect
return
return self.real_connection.connect(*args, **kwargs)
from vcr.patch import force_reset
with force_reset():
return self.real_connection.connect(*args, **kwargs)
self._sock = VCRFakeSocket()
@property
def sock(self):
if self.real_connection.sock:
return self.real_connection.sock
return VCRFakeSocket()
return self._sock
@sock.setter
def sock(self, value):
@@ -311,6 +320,8 @@ class VCRConnection(object):
with force_reset():
self.real_connection = self._baseclass(*args, **kwargs)
self._sock = None
def __setattr__(self, name, value):
"""
We need to define this because any attributes that are set on the
@@ -332,6 +343,23 @@ class VCRConnection(object):
super(VCRConnection, self).__setattr__(name, value)
def __getattr__(self, name):
"""
Send requests for weird attributes up to the real connection
(counterpart to __setattr above)
"""
if self.__dict__.get('real_connection'):
# check in case real_connection has not been set yet, such as when
# we're setting the real_connection itself for the first time
return getattr(self.real_connection, name)
return super(VCRConnection, self).__getattr__(name)
for k, v in HTTPConnection.__dict__.items():
if isinstance(v, staticmethod):
setattr(VCRConnection, k, v)
class VCRHTTPConnection(VCRConnection):
'''A Mocked class for HTTP requests'''

View File

@@ -6,59 +6,77 @@ import functools
import json
from aiohttp import ClientResponse
from yarl import URL
from vcr.request import Request
class MockClientResponse(ClientResponse):
# TODO: get encoding from header
@asyncio.coroutine
def json(self, *, encoding='utf-8', loads=json.loads): # NOQA: E999
return loads(self.content.decode(encoding))
def __init__(self, method, url):
super().__init__(
method=method,
url=url,
writer=None,
continue100=None,
timer=None,
request_info=None,
traces=None,
loop=asyncio.get_event_loop(),
session=None,
)
@asyncio.coroutine
def text(self, encoding='utf-8'):
return self.content.decode(encoding)
async def json(self, *, encoding='utf-8', loads=json.loads, **kwargs): # NOQA: E999
return loads(self._body.decode(encoding))
@asyncio.coroutine
def release(self):
async def text(self, encoding='utf-8'):
return self._body.decode(encoding)
async def read(self):
return self._body
async def release(self):
pass
def vcr_request(cassette, real_request):
@functools.wraps(real_request)
@asyncio.coroutine
def new_request(self, method, url, **kwargs):
async def new_request(self, method, url, **kwargs):
headers = kwargs.get('headers')
headers = self._prepare_headers(headers)
data = kwargs.get('data')
params = kwargs.get('params')
vcr_request = Request(method, url, data, headers)
request_url = URL(url)
if params:
for k, v in params.items():
params[k] = str(v)
request_url = URL(url).with_query(params)
vcr_request = Request(method, str(request_url), data, headers)
if cassette.can_play_response_for(vcr_request):
vcr_response = cassette.play_response(vcr_request)
response = MockClientResponse(method, vcr_response.get('url'))
response = MockClientResponse(method, URL(vcr_response.get('url')))
response.status = vcr_response['status']['code']
response.content = vcr_response['body']['string']
response._body = vcr_response['body']['string']
response.reason = vcr_response['status']['message']
response.headers = vcr_response['headers']
response._headers = vcr_response['headers']
response.close()
return response
if cassette.write_protected and cassette.filter_request(vcr_request):
response = MockClientResponse(method, url)
response = MockClientResponse(method, URL(url))
response.status = 599
msg = ("No match for the request {!r} was found. Can't overwrite "
"existing cassette {!r} in your current record mode {!r}.")
msg = msg.format(vcr_request, cassette._path, cassette.record_mode)
response.content = msg.encode()
response._body = msg.encode()
response.close()
return response
response = yield from real_request(self, method, url, **kwargs) # NOQA: E999
response = await real_request(self, method, url, **kwargs) # NOQA: E999
vcr_response = {
'status': {
@@ -66,7 +84,7 @@ def vcr_request(cassette, real_request):
'message': response.reason,
},
'headers': dict(response.headers),
'body': {'string': (yield from response.text())}, # NOQA: E999
'body': {'string': (await response.read())}, # NOQA: E999
'url': response.url,
}
cassette.append(vcr_request, vcr_response)

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',
@@ -42,7 +40,8 @@ class VCRHTTPSConnectionWithTimeout(VCRHTTPSConnection,
'timeout',
'source_address',
'ca_certs',
))
'disable_ssl_certificate_validation',
}
unknown_keys = set(kwargs.keys()) - safe_keys
safe_kwargs = kwargs.copy()
for kw in unknown_keys:

View File

@@ -1,9 +1,9 @@
'''Stubs for requests'''
try:
from requests.packages.urllib3.connectionpool import HTTPConnection, VerifiedHTTPSConnection
except ImportError:
from urllib3.connectionpool import HTTPConnection, VerifiedHTTPSConnection
except ImportError:
from requests.packages.urllib3.connectionpool import HTTPConnection, VerifiedHTTPSConnection
from ..stubs import VCRHTTPConnection, VCRHTTPSConnection