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

Compare commits

...

561 Commits

Author SHA1 Message Date
Thomas Grainger
0c4020df7d version bump 1.11.0 2017-05-02 11:36:16 +01:00
Thomas Grainger
204cb8f2ac Merge pull request #303 from graingert/support-3.6
support 3.6
2017-05-02 11:17:39 +01:00
Thomas Grainger
0e421b5327 Merge pull request #301 from graingert/handle-pytest-asyncio-coroutines
handle pytest-asyncio async def coroutines
2017-05-02 11:17:06 +01:00
Thomas Grainger
dc2dc306d5 pytest-httpbin doesn't support chunked requests on Python 3.6 2017-04-04 11:30:16 +01:00
Thomas Grainger
1092bcd1a1 add requests 2.13 2017-04-04 10:32:17 +01:00
Thomas Grainger
73dbc6f8cb add missing _get_content_length static method
also add _is_textIO
2017-04-04 10:32:17 +01:00
Thomas Grainger
3e9fb10c11 Merge remote-tracking branch 'derekbekoe/fix-vcrconnection-3.6' into support-3.6 2017-04-04 10:32:17 +01:00
Thomas Grainger
3588ed6341 support 3.6 2017-04-04 10:32:16 +01:00
Kevin McCarthy
26326c3ef0 Merge pull request #305 from graingert/add-cache-to-gitignore
add .cache to gitignore
2017-04-03 06:40:21 -10:00
Thomas Grainger
7514d94262 handle pytest-asyncio async def coroutines 2017-04-03 15:49:49 +01:00
Thomas Grainger
1df577f0fc add .cache to gitignore 2017-04-03 15:45:42 +01:00
Kevin McCarthy
70f4707063 Merge pull request #302 from AartGoossens/feature/before_record_docs_fix
Improves docs for before_record_request
2017-03-16 08:55:34 -10:00
Aart Goossens
521146d64e Improves docs for before_record_request 2017-03-16 19:25:16 +01:00
Derek Bekoe
091b402594 Revert "Add Python 3.6 to CI"
This reverts commit 24b617a427.
2017-01-24 16:26:02 -08:00
Derek Bekoe
24b617a427 Add Python 3.6 to CI 2017-01-24 13:57:37 -08:00
Derek Bekoe
97473bb8d8 Correctly patch HTTPConnection.request in Python 3.6
Fixes https://github.com/kevin1024/vcrpy/issues/293
2017-01-23 14:54:26 -08:00
Kevin McCarthy
ed35643c3e Merge pull request #292 from j-funk/master
Allow injection of persistence methods
2017-01-22 08:29:52 -06:00
Julien Funk
2fb3b52c7e add custom persister docs 2017-01-19 13:10:27 -05:00
Julien Funk
9e70993d57 substiture IOError with more appropriate ValueError 2017-01-19 13:10:08 -05:00
Julien Funk
6887e2cff9 remove unused imports 2017-01-15 15:04:20 -08:00
Julien Funk
ba38680402 Merge pull request #1 from IvanMalison/persistence_methods
Fix patch of FliesystemPersister.load_cassette
2017-01-14 15:59:13 -05:00
Ivan Malison
06b00837fc Fix patch of FliesystemPersister.load_cassette 2017-01-13 15:29:45 -08:00
Julien Funk
a033bc729c refactored, 1 failing test 2017-01-13 16:09:42 -05:00
Julien Funk
6f8486e0a2 allow injection of persistence methods 2017-01-12 16:41:26 -05:00
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
Ivan Malison
6e049ba7a1 version bump to v1.1.2 2014-10-08 12:11:53 -07:00
Ivan Malison
916e7839e5 Actually use pytest.raises in test. 2014-10-07 13:45:09 -07:00
Ivan Malison
99692a92d2 Handle unicode error in json serialize properly. 2014-10-07 13:21:47 -07:00
Ivan Malison
a9a68ba44b Random tweaks. 2014-10-05 18:37:01 -07:00
Ivan Malison
e9f35db405 Remove .travis.yml changes. 2014-10-05 16:42:46 -07:00
Ivan Malison
7193407a07 Remove ipdb because it causes python below 2.6 to blow up. 2014-10-03 01:40:02 -07:00
Ivan Malison
c3427ae3a2 Fix pip install of tox in travis. 2014-10-02 15:48:29 -07:00
Ivan Malison
3a46a6f210 travis through tox. 2014-10-02 15:26:22 -07:00
Ivan Malison
163181844b Refactor tox.ini using new 1.8 features. 2014-10-02 14:57:53 -07:00
Ivan Malison
2c6f072d11 better logging when matches aren't working. 2014-09-25 04:49:00 -07:00
Ivan Malison
361ed82a10 Bump version to 1.1.1 2014-09-22 19:22:52 -07:00
Ivan Malison
0871c3b87c Remove instance variables for filter_headers, filter_query_params, ignore_localhost and ignore_hosts. These still exist on the VCR object, but they are automatically translated into a filter function when passed to the cassette. 2014-09-22 17:57:22 -07:00
Ivan 'Goat' Malison
d484dee50f Merge pull request #110 from IvanMalison/use_cassette_decorator_pytest_compatibility
Fix use_cassette decorator in python 2 by using wrapt.decorator
2014-09-22 17:02:23 -07:00
Ivan Malison
b046ee4bb1 Fix use_cassette decorator in python 2 by using wrapt.decorator. Add wrapt as dependency. 2014-09-22 16:40:09 -07:00
Ivan 'Goat' Malison
3dea853482 Merge pull request #108 from IvanMalison/fix_CassetteContextDecorator_nesting_issues
Fix cassette context decorator nesting issues
2014-09-21 05:18:41 -07:00
Ivan Malison
113c95f971 Bump setup.py version to v1.1.0. minor tweaks to readme. 2014-09-21 05:14:30 -07:00
Ivan Malison
a2c947dc48 Fix last bit of of #109. 2014-09-21 05:06:28 -07:00
Ivan Malison
757ad9c836 Revert "Remove ConnectionRemover class that tried to get rid of vcr connections in ConnectionPools."
This reverts commit dc249b0965.

Conflicts:
	vcr/patch.py
2014-09-20 11:59:25 -07:00
Ivan Malison
18e5898ec4 Return a tuple from the _request function on CassettePatcherBuilder even if import fails. Make _recursively_apply_get_cassette_subclass actually work with dictionaries. 2014-09-20 11:28:59 -07:00
Ivan Malison
83211a1887 Make changes from b1cdd50e9b compatible with requests1.x; Update Readme.md with description of before_record_response 2014-09-20 11:05:25 -07:00
Ivan Malison
dc249b0965 Remove ConnectionRemover class that tried to get rid of vcr connections in ConnectionPools. 2014-09-19 19:02:59 -07:00
Ivan Malison
121ed79172 Mark bad test xfail. 2014-09-19 17:10:19 -07:00
Ivan Malison
b1cdd50e9b Fix some of the issues from #109 2014-09-19 17:06:53 -07:00
Ivan Malison
1018867838 Revert "Fixed issue in test_nested_context_managers_with_session_created_before_first_nesting. by using a single class and patching cassette on that class. Not a great solution :\"
This reverts commit 2bf23b2cdf.
2014-09-19 14:32:21 -07:00
Ivan Malison
b6e96020c1 Use {[testenv]deps}, instead of repeating testing requirements. Write another failing test for #109 2014-09-19 14:31:49 -07:00
Ivan Malison
8947f0fc5c Add failing test for session still being attached to cassette after context is gone. 2014-09-18 17:03:13 -07:00
Ivan Malison
2bf23b2cdf Fixed issue in test_nested_context_managers_with_session_created_before_first_nesting. by using a single class and patching cassette on that class. Not a great solution :\ 2014-09-18 17:01:48 -07:00
Ivan Malison
58fcb2b453 Add test that fails because of the fact that a new class is used for each cassette context instead of replacing the cassette on the existing mock connection. 2014-09-18 16:17:48 -07:00
Ivan Malison
0c19acd74f Use contextdecorator from contextlib2. add logging for entering context. 2014-09-18 15:25:42 -07:00
Ivan Malison
4868a63876 Refactor build_patchers into class. Fix issue with patching non existent attribute with hasattr. 2014-09-18 14:42:56 -07:00
Ivan Malison
e1e08c7a2c hasattr check for requests 2.0 use cassette added type for httplib2 dictionary patch. 2014-09-18 08:02:50 -07:00
Ivan Malison
5edc58f10c Check for old style class when building subclass. 2014-09-18 07:10:52 -07:00
Ivan Malison
2193008150 Python version agnostic way of getting the next item in the generator. 2014-09-18 05:59:03 -07:00
Ivan Malison
958aac3af3 Use mock for patching http connection objects. 2014-09-18 05:32:55 -07:00
Ivan Malison
9a564586a4 Failing tests for nested context decoration. 2014-09-18 03:46:39 -07:00
Ivan Malison
643a4c91ee Change use_cassette to pass a function to CassetteContextDecorator so that changes to the default settings on the vcr properly propogate. 2014-09-18 02:52:44 -07:00
Ivan Malison
472cc3bffe use_cassette -> CassetteContextDecorator 2014-09-17 23:22:43 -07:00
Ivan Malison
8db46002a3 Fix failure in test_local_host resulting from attempting to add tuple to list. 2014-09-17 21:48:28 -07:00
Ivan Malison
a08c90c5d6 Revert "Add global toggle to use_cassette."
This reverts commit 366e2b75bb.

Conflicts:
	tests/unit/test_cassettes.py
2014-09-17 21:42:25 -07:00
Ivan Malison
8e01426056 Change default paramters to VCR from lists to tuples. 2014-09-17 19:29:02 -07:00
Ivan Malison
9a4f5f23a4 Add before_record_response to Cassette and VCR. 2014-09-17 04:10:05 -07:00
Ivan Malison
366e2b75bb Add global toggle to use_cassette. 2014-09-17 01:28:54 -07:00
Ivan Malison
0cfe63ef6e Bump version number for new use_cassette_decorator. 2014-09-16 23:53:50 -07:00
Ivan Malison
cb05f4163c Add use_cassette class so functinos that are decorated with use_cassette can be called multiple times. 2014-09-16 23:45:05 -07:00
Kevin McCarthy
20057a6815 Merge pull request #101 from IvanMalison/minor_cleanup
Clean up __init__.py .
2014-09-15 19:10:40 -10:00
Ivan Malison
0d313502b8 Clean up __init__.py . 2014-09-15 21:10:24 -07:00
Kevin McCarthy
d9c2b4b25d Merge pull request #100 from IvanMalison/immutable_keyword_defaults
Make defaults of Cassette constructor immutable. Clean up whitespace. Re...
2014-09-12 14:01:55 -10:00
Ivan Malison
640681138a Make defaults of Cassette constructor immutable. Clean up whitespace. Remove unused import. 2014-09-12 02:16:32 -07:00
Kevin McCarthy
a02bbbab2b Merge pull request #97 from matt-thomson/multiple-headers
Fix serialization/deserialization of multiple headers with the same value
2014-09-07 10:06:26 -10:00
Matt Thomson
f719f90e63 Fix multiple header behaviour.
Join multiple header values together, rather than losing duplicates with a
dict.
2014-09-06 17:15:15 +01:00
Matt Thomson
3c410b5f9d Don't write header values multiple times.
On Python 3, response.msg.keys() contains the same value multiple times if
there are multiple headers with the same value.  Work around this by
converting to a set before iterating over it.
2014-09-06 16:57:12 +01:00
Matt Thomson
7a5795a547 Add test to demonstrate Python 3 multiple headers bug. 2014-09-06 16:32:29 +01:00
Kevin McCarthy
1bd3fbd2c6 bump version 2014-09-01 15:50:25 -10:00
Kevin McCarthy
cd715f37c6 Fix requests stub. Closes #94 2014-09-01 15:48:57 -10:00
Kevin McCarthy
9a1147196a getheader() in stubs should be case-insensitive 2014-08-01 16:28:21 -10:00
Kevin McCarthy
a23c5d8508 Merge pull request #87 from hartsock/master
Python 3: print_function
2014-07-24 18:10:24 -10:00
Shawn Hartsock
868a974900 Python 3: print_function
Use print function if you must print, this lets us use the
library in python 3 environments.

partial: https://github.com/kevin1024/vcrpy/issues/86
2014-07-24 16:33:47 -04:00
Kevin McCarthy
c56de472cd Remove extra colon in README
Closes #82
2014-07-07 16:02:29 -10:00
Kevin McCarthy
c6590f2caf bump version 2014-05-17 13:09:10 -10:00
Kevin McCarthy
70abc5058c requests 2.3 compat 2014-05-17 12:58:31 -10:00
Kevin McCarthy
0c1f1e2479 Version bump to 1.0.1 2014-05-17 09:44:02 -10:00
Kevin McCarthy
8d90dba16c Ignore requests before trying to play them
Closes #79
2014-05-17 09:34:50 -10:00
Kevin McCarthy
3072c56ed2 Update README.md 2014-05-12 09:22:09 -10:00
Kevin McCarthy
c84fb1886b bump version 2014-05-10 12:01:19 -10:00
Kevin McCarthy
3b05d499c3 Merge pull request #78 from mshytikov/feature/cassette-all-played
Feature/Cassette#all_played
2014-05-10 11:58:28 -10:00
Kevin McCarthy
8eb54c012f pep8 2014-05-10 11:52:36 -10:00
Kevin McCarthy
6d656717a1 gotta think of the future 2014-05-10 11:52:36 -10:00
Kevin McCarthy
d7f7152dbf add note about hack 2014-05-10 11:52:36 -10:00
Kevin McCarthy
483554ed2e more migration replacement strings 2014-05-10 11:52:36 -10:00
Kevin McCarthy
83ff73792e forgot to check in tests 2014-05-10 11:52:36 -10:00
Kevin McCarthy
fd30689c12 check for old cassette version 2014-05-10 11:52:36 -10:00
Kevin McCarthy
62f9c528b5 string replace frozenset 2014-05-10 11:52:36 -10:00
Kevin McCarthy
007fa851ed refactor migration script to reuse code 2014-05-10 11:52:36 -10:00
Kevin McCarthy
ffdba74299 remove pdb 2014-05-10 11:52:36 -10:00
Kevin McCarthy
2c895eb5e9 fix serialization problem 2014-05-10 11:52:36 -10:00
Kevin McCarthy
b671e7ab99 headers dont retain order anymore.... sigh 2014-05-10 11:52:35 -10:00
Kevin McCarthy
b36a1157e7 Just make all the headers lower for now 2014-05-10 11:52:35 -10:00
Kevin McCarthy
0c9761f7ff encoooode 2014-05-10 11:52:35 -10:00
Kevin McCarthy
66c6909021 not really very happy about this 2014-05-10 11:52:35 -10:00
Kevin McCarthy
c0691a96e6 flickr test was actually using yaml cassettes 2014-05-10 11:52:35 -10:00
Kevin McCarthy
a302874c6d update old cassette detection 2014-05-10 11:52:35 -10:00
Kevin McCarthy
b43c63f284 update serializers 2014-05-10 11:52:35 -10:00
Kevin McCarthy
2c33ae2664 fix a couple imports for py3 2014-05-10 11:52:35 -10:00
Kevin McCarthy
e50f917cf4 Make Serializers Dumber
Let's have the serializer just worry about serializing the dict
that we hand it, and move the unicode stuff up to a serialize module.

This should hopefully let us move toward using a version string in
cassettes.
2014-05-10 11:52:35 -10:00
Kevin McCarthy
4ab46f9643 fix python3 compat 2014-05-10 11:52:35 -10:00
Kevin McCarthy
23b5d49736 fix migration script 2014-05-10 11:52:35 -10:00
Kevin McCarthy
bc45a965b2 remove some unused serialization code 2014-05-10 11:52:35 -10:00
Kevin McCarthy
2da49884be update wild cassette to match new format 2014-05-10 11:52:35 -10:00
Kevin McCarthy
1e8e7057f5 fix python3 compat 2014-05-10 11:52:35 -10:00
Kevin McCarthy
d1a3ab56b1 update migration fixtures 2014-05-10 11:52:34 -10:00
Kevin McCarthy
a65da12aeb change response format 2014-05-10 11:52:34 -10:00
Kevin McCarthy
1f99ede46f lets try response headers as dicts 2014-05-10 11:52:34 -10:00
Kevin McCarthy
f479f205ad Update copyright date 2014-05-07 16:28:26 -10:00
Kevin McCarthy
c72d19175f Update README.md 2014-05-05 18:40:56 -10:00
Max Shytikov
52c9bf04fe Added implementation for Cassette#all_payed 2014-05-06 00:46:43 +02:00
Max Shytikov
bd2121d34e pep8 2014-05-06 00:46:32 +02:00
Max Shytikov
f9f2b98427 Added tests for Cassette#all_played 2014-05-06 00:46:03 +02:00
Max Shytikov
9d5660b673 Added README for new 'all_played' property of Cassette 2014-05-06 00:45:10 +02:00
Kevin McCarthy
5e295e0603 Serialize dict of lists, use dicts internally
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.
2014-05-03 17:39:12 -10:00
Kevin McCarthy
2ef5f9208a install pytest-localserver on travis 2014-05-03 15:53:20 -10:00
Kevin McCarthy
0be7d6f238 oops, forgot to commit new tests for ignore feature 2014-05-03 15:52:45 -10:00
Kevin McCarthy
3990b32892 New Feature: Ignore Some Requests
Add 2 new options, ignore_localhost and ignore_hosts, which can ignore
requests so they aren't recorded in a cassette.

Closes #74
2014-05-03 15:25:31 -10:00
Kevin McCarthy
b5cfd517cf update README 2014-05-03 15:21:05 -10:00
Kevin McCarthy
adf127c073 Merge pull request #73 from mshytikov/feature/new-matchers
Feature/new matchers
2014-05-03 15:11:02 -10:00
Max Shytikov
78f6ce46b5 Added test casses and refactored test for Request#port 2014-05-04 02:20:46 +02:00
Max Shytikov
ce5d2225a6 pep8 2014-05-04 02:12:48 +02:00
Max Shytikov
3322234b25 Updated Request with stup to support default ports 2014-05-04 02:12:38 +02:00
Max Shytikov
5d10a38160 Updated migration to support default ports 2014-05-04 02:11:49 +02:00
Max Shytikov
0b1aeac25e Renamed outdated url to uri. 2014-05-04 02:11:49 +02:00
Max Shytikov
1190a0e62e Removed default '80' port of uri in tests 2014-05-04 02:11:49 +02:00
Max Shytikov
61e3bdc402 Added tetst for uri and port of Request 2014-05-03 22:31:30 +02:00
Max Shytikov
1ff5d08c8b Fixed typo 2014-05-02 07:32:18 +02:00
Max Shytikov
1d8e2dbb41 Removed accidental print call 2014-05-02 07:32:18 +02:00
Max Shytikov
62d19e5cc1 Update _remove_headers to work with headers copy
Because of the filter implementation here we nedd to work only
with clone of the headers and request. subj to refactor
2014-05-02 07:32:18 +02:00
Max Shytikov
3d2da26933 Updated test to use new headers structure 2014-05-02 07:32:18 +02:00
Max Shytikov
65c2797f94 Updated test for filters. Mock replaced with real Request object 2014-05-02 07:32:18 +02:00
Max Shytikov
998dde61ec Updated _remove_query_parameters to use latest Request Api 2014-05-02 07:32:17 +02:00
Max Shytikov
faa83b9aba Fixed name of the variable 2014-05-02 07:32:17 +02:00
Max Shytikov
a48f621bae Updated _remove_headers to use latest Headers structure
Probably we need API in Request object like 'remove_header'
2014-05-02 07:32:17 +02:00
Max Shytikov
fbd5049d38 Updated test to use new Request constructor 2014-05-02 07:32:17 +02:00
Max Shytikov
7b253ebc6f pep8 2014-05-02 07:32:17 +02:00
Max Shytikov
5d1f35973d Make code 2.6 compatible 2014-05-02 07:32:17 +02:00
Max Shytikov
fbb6382c12 Added migration for Request headers to be a dict of lists 2014-05-02 07:32:17 +02:00
Max Shytikov
eab10578d5 Make Request headers to be a dict of lists 2014-05-02 07:32:15 +02:00
Max Shytikov
e4d1db0617 Removed frozenset 2014-04-30 02:38:01 +02:00
Max Shytikov
7e677f516d Deleted unnecessary __hash__ method of Request 2014-04-30 02:36:27 +02:00
Max Shytikov
34ce0a35ec Updated wild fixtures in correspondence with new fixture format 2014-04-30 02:36:27 +02:00
Max Shytikov
b6195bf41e Updated migration fixtures in correspondence with new fixture format 2014-04-30 02:36:27 +02:00
Max Shytikov
eedafb19ee Added more test for persist 2014-04-30 02:36:27 +02:00
Max Shytikov
434d6325ea Udated migration test for yaml. replaced strict content comparision
What we care about it is actually data after loading not the
strict format of yaml file
2014-04-30 02:36:27 +02:00
Max Shytikov
25c0141e27 Updated migration script to use new yaml serialization 2014-04-30 02:36:26 +02:00
Max Shytikov
1e995c3c9b Replaced yaml dump of Request object with plain dict dump 2014-04-30 02:36:26 +02:00
Max Shytikov
0408bdaadb Added checkfor old cassette on load cassete to persist module 2014-04-30 02:36:26 +02:00
Max Shytikov
9c9612f93e Fixed crazy/stupid implementation mistakes 2014-04-30 02:36:26 +02:00
Max Shytikov
a3eac1f0ec Added tests for persist module 2014-04-30 02:36:26 +02:00
Max Shytikov
710ec6f432 Added "New Cassette Format" section to readme 2014-04-30 02:36:26 +02:00
Max Shytikov
9d8426e668 Make migration python 2.6 compatible 2014-04-30 02:36:26 +02:00
Max Shytikov
424c658da4 Fixed open tmp file in python3 2014-04-30 02:36:26 +02:00
Max Shytikov
f0972628ef Fixed migration for one file 2014-04-30 02:36:26 +02:00
Max Shytikov
ee28768a31 Added migration script for old cassettes 2014-04-30 02:36:26 +02:00
Max Shytikov
5354ef781c Formatted setup.py to make flake8 happy 2014-04-30 02:36:25 +02:00
Max Shytikov
750e141b9d Added integration tests for matchers 2014-04-30 02:36:25 +02:00
Max Shytikov
a042cb3824 Updated README 2014-04-30 02:36:25 +02:00
Max Shytikov
96d8782d08 Added 'protocol' to Request for backwards compatibility 2014-04-30 02:31:11 +02:00
Max Shytikov
f9a64e1609 Fixed available matchers declaration 2014-04-30 02:31:11 +02:00
Max Shytikov
2fa1aaa1f7 Replaced 'url' mather with 'uri'. 2014-04-30 02:31:11 +02:00
Max Shytikov
7fe55ad8b8 Updated 'set' to be compatible with 2.6 2014-04-30 02:30:08 +02:00
Max Shytikov
2f6db0dc0c Added scheme to default 'match_on' 2014-04-30 02:30:08 +02:00
Max Shytikov
4267828a3e Added 'scheme' to Request with matcher and test 2014-04-30 02:29:25 +02:00
Max Shytikov
4e9d5f6885 Updated default 'match_on' 2014-04-30 02:29:25 +02:00
Max Shytikov
5015dbd878 Improved test samples 2014-04-30 02:27:52 +02:00
Max Shytikov
9b188e986f Added query to Request with matcher and test 2014-04-30 02:27:52 +02:00
Max Shytikov
6b060e5666 Added path to Request with matcher and test 2014-04-30 02:27:52 +02:00
Max Shytikov
bd9fa773e8 Added port to Request with matcher and test 2014-04-30 02:27:52 +02:00
Max Shytikov
18ec57fa73 Added test and impl for Request 'host' attribute 2014-04-30 02:27:52 +02:00
Max Shytikov
6cca703eee Refactored unit test for matchers 2014-04-30 02:27:52 +02:00
Max Shytikov
edf1df9188 Replaced Request 'host, port, protocol, path' with 'uri' 2014-04-30 02:27:52 +02:00
Max Shytikov
e0c6a8429d Added integration test for match on 'url' 2014-04-30 02:27:51 +02:00
Max Shytikov
08d4d8913a Added integration test for match on 'method' 2014-04-30 02:27:51 +02:00
Max Shytikov
792d665893 Added unit test for matcher 'url' 2014-04-30 02:27:51 +02:00
Max Shytikov
cd32f5114c Added unit test for matcher 'method' 2014-04-30 02:27:51 +02:00
Max Shytikov
16c6135387 Removed 'serializer' from name of test functions
Because the name '_serializer_' has no relationships with this tests
2014-04-30 02:27:51 +02:00
Kevin McCarthy
5aa2fb017f Add waffle badge 2014-04-27 12:05:19 -10:00
Kevin McCarthy
e6fdc735e4 Filter Sensitive Data From Requests
Add the ability to filter out sensitive data, using one of three
methods: from headers, from a query string, and by using a custom
callback to modify the request.

Closes #67
2014-04-27 11:38:28 -10:00
Kevin McCarthy
f317800cb7 Add Logging
This helps to figure out which matcher has decided your two cassettes
differ, and figure out when your cassettes have hit the network.

Closes #34
2014-04-27 11:29:06 -10:00
Kevin McCarthy
4302d7753e clean up readme whitespace 2014-04-26 22:28:38 -10:00
Kevin McCarthy
58ac00a7f6 pep8 2014-04-26 21:30:43 -10:00
Kevin McCarthy
ab27c71a81 remove failing test since I'm not going to fix this use case 2014-04-26 18:49:40 -10:00
Kevin McCarthy
6273c32334 Add Exception when JSON Serializing Binary Data
Since I can't think of a good way to deal with this, let's just give a
nice error message to point people in the right direction.

Closes #51
2014-04-26 14:31:40 -10:00
Kevin McCarthy
2a072e4dd3 fix tox envlist 2014-04-25 18:55:43 -10:00
Kevin McCarthy
582b8eab51 pep8 2014-04-23 21:24:12 -10:00
Kevin McCarthy
e002aab999 let's keep testing python3.3, at least for the time being. 2014-04-23 19:36:31 -10:00
Kevin McCarthy
bc5199c893 improve socket mocking to hopefully pass both new python3.4 socket connection stuff and requests tests 2014-04-23 19:30:43 -10:00
Kevin McCarthy
bd2d2cea72 bump supported python3 version to 3.4 2014-04-23 18:58:17 -10:00
Kevin McCarthy
59f3216d44 Merge pull request #75 from simon-weber/patch-1
fix cassette url field capitalization
2014-04-11 21:43:36 -10:00
Simon Weber
23d1717216 fix cassette url field capitalization 2014-04-11 16:43:41 -04:00
Kevin McCarthy
1452455d0b Merge pull request #72 from marusich/master
Add full support for Boto
2014-04-05 19:40:22 -10:00
Chris Marusich
955e532162 Add full support for Boto
Before this change, vcrpy would not work with modules of Boto (e.g., boto.iam)
that use Boto's CertValidatingHTTPSConnection to connect to AWS (unless you
went through the extra effort of disabling certificate validation during the
tests).  This change adds support for those modules.
2014-04-03 13:52:30 -07:00
Chris Marusich
20ff2e9d9a Add a failing test to illustrate a hole in vcrpy's Boto support
This test will fail with the following error:

TypeError: unbound method __init__() must be called with VCRHTTPConnection
instance as first argument (got CertValidatingHTTPSConnection instance instead)

The TypeError is raised because the __init__ method of Boto's
CertValidatingHTTPSConnection (which extends httplib.HTTPConnection) calls
httplib.HTTPConnection.__init__, and during the test httplib.HTTPConnection
actually refers to the patched verison (i.e., VCRHTTPConnection).  When
VCRHTTPConnection.__init__ is called, it expects to receive a
VCRHTTPConnection object as its first argument, but instead it receives a
CertValidatingHTTPSConnection object.  Because the only ancestor class of
CertValidatingHTTPSConnection is the original, un-patched
httplib.HTTPConnection, the first argument is not considered to be a
VCRHTTPConnection object, so a TypeError is raised.
2014-04-03 13:21:33 -07:00
Kevin McCarthy
544d2127d3 beef up the new epsisodes test a bit 2014-03-29 12:07:21 -10:00
Kevin McCarthy
01901b7a4e Revert "Merge pull request #68 from smallcode/patch-1"
Never mind, I do seem to have support for this.  I'm not sure
why @smallcode was having issues with it, but I'm going to
beef up the test for it a bit to try to figure out what's broken.

This reverts commit c83134ca39, reversing
changes made to b7cbd181f4.
2014-03-29 12:03:38 -10:00
Kevin McCarthy
c83134ca39 Merge pull request #68 from smallcode/patch-1
new_episodes record mode has been unable to use
2014-03-21 19:35:20 -10:00
smallcode
ebb180f7a5 new_episodes has been unable to use
I just found that it has been unable to use, is it?
2014-03-19 23:10:24 +08:00
Kevin McCarthy
b7cbd181f4 bump version to v0.7.0 2014-03-09 15:30:41 -10:00
Kevin McCarthy
4a4b04e5a6 Update README.md 2014-03-09 15:22:02 -10:00
Kevin McCarthy
6d0a8d8ed9 use six.moves instead of _compat 2014-03-08 23:14:16 -10:00
Kevin McCarthy
985e573303 pep8 cleanup 2014-03-08 22:59:10 -10:00
Kevin McCarthy
73666bcb49 add AWS keys to travis 2014-03-08 22:49:01 -10:00
Kevin McCarthy
f5db99f079 need the sock for httplib2, don't want sock for requests, can't we all just get along? 2014-03-08 21:56:25 -10:00
Kevin McCarthy
dedca0f6e7 expose sock, pass correct args to connect in stubs. should fix httplib2 in python3 2014-03-08 21:27:09 -10:00
Kevin McCarthy
0d77d2dcc6 fix self.closed in python3, and request must inherit from httprequest for httplib2 2014-03-08 21:11:50 -10:00
Kevin McCarthy
56a9a53522 add more tox envs 2014-03-08 20:58:41 -10:00
Kevin McCarthy
34e7760d47 let's add boto as a supported client, and maybe we dont need the empty travis env anymore 2014-03-08 20:21:20 -10:00
Kevin McCarthy
32b99f0719 urlencode moved in py3 2014-03-08 20:21:04 -10:00
Kevin McCarthy
d187d910b9 Don't try to inherit from the real response object 2014-03-08 20:16:45 -10:00
Åsmund Grammeltvedt
a73da71159 Add Python 2.3 support
This commit also adds the 'six' dependency
2014-03-08 20:01:48 -10:00
Kevin McCarthy
2385176084 Cut down on the number of environments in Travis 2014-03-08 19:35:32 -10:00
Kevin McCarthy
14590ae3c7 Add httplib2 tests to travis 2014-03-08 19:35:32 -10:00
Kevin McCarthy
d16b20a780 restore tox envs 2014-03-08 19:35:31 -10:00
Roberto Abdelkader Martínez Pérez
46a2c25f6a httplib2 support 2014-03-08 19:35:30 -10:00
Kevin McCarthy
6bb67567f9 add tests for boto 2014-03-08 19:24:11 -10:00
Kevin McCarthy
e84cd6f059 Major Refactor of Stubs
So the stubs were getting out of hand, and while trying to add support for the
putrequest and putheader methods, I had an idea for a cleaner way to handle
the stubs using the VCRHTTPConnection more as a proxy object.  So
VCRHTTPConnection and VCRHTTPSConnection no longer inherit from HTTPConnection
and HTTPSConnection.  This allowed me to get rid of quite a bit of
copy-and-pasted stdlib code.
2014-03-08 19:22:58 -10:00
Kevin McCarthy
c0b88c2201 Merge pull request #65 from msabramo/patch-1
README.md: minor formatting, add links
2014-03-08 19:18:14 -10:00
Marc Abramowitz
f003e3e4ab README.md: minor formatting, add links 2014-03-03 06:26:57 -08:00
Kevin McCarthy
d0e6f9c047 Add note to README about tox usage 2014-02-09 08:49:58 -10:00
Kevin McCarthy
1298d6f5c7 Merge pull request #61 from msabramo/tox_posargs
tox.ini: Add {posargs} for passing args to py.test
2014-02-09 08:49:22 -10:00
Kevin McCarthy
df67dd1728 Merge pull request #63 from msabramo/issue_59_save_requests_HTTPSConnection
patch: Save requests...HTTPSConnection
2014-02-04 10:49:28 -08:00
Marc Abramowitz
559cd902e1 patch: Save requests...HTTPSConnection
so that we unpatch back to the correct class in reset().

Closes #59
2014-02-04 10:30:29 -08:00
Marc Abramowitz
c44fee1f16 tox.ini: Add {posargs} for passing args to py.test
This allows you to do stuff like:

   tox -e py26requests,py27requests,pypyrequests -- tests/integration/test_requests.py
2014-02-04 00:25:13 -08:00
Kevin McCarthy
8620bc3af1 Update README.md 2014-01-11 09:34:40 -10:00
Kevin McCarthy
84bf7b6132 Load Old Cassettes
Add backwards-compatible cassette loading code that can load
the old style header dicts from response cassettes.
2014-01-11 09:28:45 -10:00
Kevin McCarthy
2cf779d776 PEP8 Fixes 2013-12-19 19:43:32 -10:00
Kevin McCarthy
d4494bae50 Let's have the new exceptions subclass basic exception types 2013-12-19 19:43:32 -10:00
Marc Abramowitz
41f5fce895 Nicer error for can't overwrite existing cassette
Raise CannotOverwriteExistingCassetteException rather than Exception.
Include cassette filename and record mode in error message.
2013-12-19 19:20:00 -10:00
Marc Abramowitz
a6806f2f99 Nicer error when cassette doesn't contain request
Raise UnhandledHTTPRequestError.
Show name of cassette and request.
2013-12-19 15:41:04 -08:00
Kevin McCarthy
624212ef15 Store Headers as a List
This is a backwards-incompatible change that will store headers
as a list rather than a dictionary.  The reason being that you can
have multiple values for a single header, and concatenating them
together with commas can create an unparseable string (sometimes
the header values can have commas in them)
2013-12-15 16:56:39 -10:00
Kevin McCarthy
144d25bc66 Add Decorator Support 2013-12-15 16:56:25 -10:00
Kevin McCarthy
0d08157e5d Removing Pypy tests until travis installs version 2.2.1 2013-12-15 16:39:44 -10:00
Kevin McCarthy
ea24854093 Version bump to 0.5.0 2013-12-01 14:51:29 -10:00
Kevin McCarthy
49929e3064 formatting fixes 2013-12-01 14:38:46 -10:00
Kevin McCarthy
188b57a2fa Fix API by adding 'responses_of' method
`responses_of` replaces `response_of`, since each request can have
several matching responses now.

This breaks backwards compatibility if you are using the
response_of method, so a version bump will be required.
2013-12-01 14:26:35 -10:00
Kevin McCarthy
b84f8e963b Fix Cryptic 'write-protected' Message
Closes #46
2013-12-01 13:46:44 -10:00
Marc Abramowitz
ea13d51677 del self.sock in VCRConnectionMixin.request
instead of in connection class constructors.

Fixes GH-48.
2013-11-26 12:12:39 -08:00
Marc Abramowitz
5ba4000f77 Add test: test_session_and_connection_close
This is a test for issue GH-48.
2013-11-26 12:07:13 -08:00
Kevin McCarthy
a4844d972b Version Bump to 0.4.0 2013-11-10 12:37:54 -10:00
Kevin McCarthy
c2d857c585 Remove Secure File Overwrite Support
Closes #42
2013-11-10 12:25:59 -10:00
Kevin McCarthy
89403c255c Record Multiple Matching Requests
This change allows us to record multiple matching requests to
the same URL, and then play them back sequentially.

Closes #40, #41
2013-11-10 12:17:39 -10:00
Veros Kaplan
d50ded68ca Added test: accessing same recource three times in once mode 2013-11-10 10:54:09 -10:00
Veros Kaplan
16fa4f851d Added test: accessing two times same page 2013-11-10 10:54:09 -10:00
Kevin McCarthy
6200493896 Remove Some stray \t characters
I guess that's what I get for playing around with my vimrc.

Thanks to @bryanhelmig for pointing these out.
2013-11-09 17:52:02 -10:00
Kevin McCarthy
b0a13ba690 Fix Requests so it can gunzip the request body
I wasn't emulating the stateful file-object in my response stub,
so urllib3 wasn't decompressing gzipped bodies properly.  This
should fix that problem.

Thanks @bryanhelmig for the motivation to dig into this.
2013-11-09 17:51:29 -10:00
Kevin McCarthy
d33b19b5bb Fix Requests 2, Version Bump to 0.3.5
This fixes a compatiblity issue with the new version of requests.
Bumps the release version to 0.3.5, and closes #39.
2013-10-24 21:57:18 -10:00
96 changed files with 7814 additions and 1599 deletions

4
.gitignore vendored
View File

@@ -1,8 +1,12 @@
*.pyc
.tox
.cache
build/
dist/
*.egg/
.coverage
*.egg-info/
pytestdebug.log
fixtures/
/docs/_build

View File

@@ -1,13 +1,69 @@
language: python
sudo: false
before_install: openssl version
env:
- WITH_REQUESTS="True"
- WITH_REQUESTS="False"
global:
- secure: AifoKzwhjV94cmcQZrdQmqRu/9rkZZvWpwBv1daeAQpLOKFPGsOm3D+x2cSw9+iCfkgDZDfqQVv1kCaFVxTll8v8jTq5SJdqEY0NmGWbj/UkNtShh609oRDsuzLxAEwtVKYjf/h8K2BRea+bl1tGkwZ2vtmYS6dxNlAijjWOfds=
- 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="requests213"
- 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="tornado4"
- TOX_SUFFIX="aiohttp"
matrix:
allow_failures:
- env: TOX_SUFFIX="boto"
- env: TOX_SUFFIX="boto3"
exclude:
- env: TOX_SUFFIX="flakes"
python: 2.6
- env: TOX_SUFFIX="boto"
python: 3.3
- env: TOX_SUFFIX="boto"
python: 3.4
- env: TOX_SUFFIX="boto"
python: 3.6
- env: TOX_SUFFIX="requests1"
python: 3.4
- env: TOX_SUFFIX="requests1"
python: 3.5
- env: TOX_SUFFIX="requests1"
python: 3.6
- env: TOX_SUFFIX="aiohttp"
python: 2.6
- env: TOX_SUFFIX="aiohttp"
python: 2.7
- env: TOX_SUFFIX="aiohttp"
python: 3.3
- env: TOX_SUFFIX="aiohttp"
python: pypy
- env: TOX_SUFFIX="aiohttp"
python: pypy3
python:
- 2.6
- 2.7
- pypy
install:
- pip install PyYAML pytest --use-mirrors
- if [ $WITH_REQUESTS = "True" ] ; then pip install requests; fi
script: python setup.py test
- 2.6
- 2.7
- 3.3
- 3.4
- 3.5
- 3.6
- pypy
- pypy3
install:
- 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-2013 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]

310
README.md
View File

@@ -1,310 +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/myronmarston/vcr).
[![Build Status](https://secure.travis-ci.org/kevin1024/vcrpy.png?branch=master)](http://travis-ci.org/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
This should work with Python 2.6 and 2.7, and [pypy](http://pypy.org).
Currently I've only tested this with urllib2, urllib3, and requests. It's known to *NOT WORK* with urllib.
##How to use it
```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).
## 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 = ['url', '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 `['url', method']`
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)
* url (the full URL, including the protocol)
* host (the hostname of the server receiving the request)
* path (excluding the hostname)
* body (the entire request body)
* headers (the headers of the request)
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 url should have been http://www.zombo.com/
assert cass.requests[0].url == '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 containing the requests made
while this cassette was being used, ordered by the order that the
request was made.
* `responses`: A list of the responses made.
* `play_count`: The number of times this cassette has had a response
played back
* `play_counts`: A collections.Counter showing the number of times each
response has been played back, indexed by the request
* `response_of(request)`: Access the response for a given request.
The Request object has the following properties
* `URL`: The full url of the request, including the protocol. Example: "http://www.google.com/"
* `path`: The path of the request. For example "/" or "/home.html"
* `host`: The host of the request, for example "www.google.com"
* `port`: The port the request was made on
* `method` : The method used to make the request, for example "GET" or "POST"
* `protocol`: The protocol used to make the request (http or https)
* `body`: The body of the request, usually empty except for POST / PUT / etc
## 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
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.url == r2.url 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
```
##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
I'm not trying to match the format of the Ruby VCR YAML files. Cassettes generated by
Ruby's VCR are not compatible with VCR.py.
##Known Issues
This library is a work in progress, so the API might change on you.
There are probably some [bugs](https://github.com/kevin1024/vcrpy/issues?labels=bug&page=1&state=open) floating around too.
##Changelog
* 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
##Similar libraries in Python
Neither of these really implement the API I want, but I have cribbed some code
from them.
* https://github.com/bbangert/Dalton
* https://github.com/storborg/replaylib
These were created after I created VCR.py but do something similar:
* https://github.com/gabrielfalcao/HTTPretty
* https://github.com/kanzure/python-requestions
* https://github.com/uber/cassette
#License
This library uses the MIT license. See [LICENSE.txt](LICENSE.txt) for more details

72
README.rst Normal file
View File

@@ -0,0 +1,72 @@
|PyPI| |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.
Support
-------
VCR.py works great with the following HTTP clients:
- requests
- aiohttp
- urllib3
- tornado
- urllib2
- boto
- boto3
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-unittest
.. |Build Status| image:: https://secure.travis-ci.org/kevin1024/vcrpy.png?branch=master
:target: http://travis-ci.org/kevin1024/vcrpy
.. |Waffle Ready| image:: https://badge.waffle.io/kevin1024/vcrpy.png?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__

165
docs/changelog.rst Normal file
View File

@@ -0,0 +1,165 @@
Changelog
---------
- 1.11.0 Allow injection of persistence methods + bugfixes (thanks @j-funk and @IvanMalison),
Support python 3.6 + CI tests (thanks @derekbekoe and @graingert),
Support pytest-asyncio coroutines (thanks @graingert)
- 1.10.5 Added a fix to httplib2 (thanks @carlosds730), Fix an issue with
aiohttp (thanks @madninja), Add missing requirement yarl (thanks @lamenezes),
Remove duplicate mock triple (thanks @FooBarQuaxx)
- 1.10.4 Fix an issue with asyncio aiohttp (thanks @madninja)
- 1.10.3 Fix some issues with asyncio and params (thanks @anovikov1984 and
@lamenezes), Fix some issues with cassette serialize / deserialize and empty
response bodies (thanks @gRoussac and @dz0ny)
- 1.10.2 Fix 1.10.1 release - add aiohttp support back in
- 1.10.1 [bad release] Fix build for Fedora package + python2 (thanks @puiterwijk and @lamenezes)
- 1.10.0 Add support for aiohttp (thanks @lamenezes)
- 1.9.0 Add support for boto3 (thanks @desdm, @foorbarna). Fix deepcopy issue
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`

84
docs/installation.rst Normal file
View File

@@ -0,0 +1,84 @@
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.6 and 2.7, 3.3, 3.4, and
`pypy <http://pypy.org>`__.
The following http libraries are supported:
- urllib2
- urllib3
- http.client (python3)
- requests (both 1.x and 2.x versions)
- httplib2
- boto
- Tornado's AsyncHTTPClient
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-dev # 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 (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.
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 $1

2
setup.cfg Normal file
View File

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

View File

@@ -1,8 +1,13 @@
#!/usr/bin/env python
import sys
from setuptools import setup
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()
class PyTest(TestCommand):
@@ -13,41 +18,66 @@ class PyTest(TestCommand):
self.test_suite = True
def run_tests(self):
#import here, cause outside the eggs aren't loaded
# import here, cause outside the eggs aren't loaded
import pytest
errno = pytest.main(self.test_args)
sys.exit(errno)
setup(name='vcrpy',
version='0.3.4',
description="A Python port of Ruby's VCR to make mocking HTTP easier",
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'],
license='MIT',
tests_require=['pytest','mock'],
cmdclass={'test': PyTest},
classifiers=[
'Development Status :: 4 - Beta',
'Environment :: Console',
'Intended Audience :: Developers',
'Programming Language :: Python',
'Topic :: Software Development :: Testing',
'Topic :: Internet :: WWW/HTTP',
'License :: OSI Approved :: MIT License',
],
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'],
':python_version in "3.4, 3.5, 3.6"': ['yarl'],
}
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)
excluded_packages = ["tests*"]
if sys.version_info[0] == 2:
excluded_packages.append("vcr.stubs.aiohttp_stubs")
setup(
name='vcrpy',
version='1.11.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=find_packages(exclude=excluded_packages),
install_requires=install_requires,
extras_require=extras_require,
license='MIT',
tests_require=['pytest', 'mock', 'pytest-httpbin'],
classifiers=[
'Development Status :: 4 - Beta',
'Environment :: Console',
'Intended Audience :: Developers',
'Programming Language :: Python',
'Programming Language :: Python :: 3',
'Topic :: Software Development :: Testing',
'Topic :: Internet :: WWW/HTTP',
'License :: OSI Approved :: MIT License',
]
)

View File

@@ -1,3 +1,6 @@
import json
def assert_cassette_empty(cass):
assert len(cass) == 0
assert cass.play_count == 0
@@ -6,3 +9,11 @@ def assert_cassette_empty(cass):
def assert_cassette_has_one_response(cass):
assert len(cass) == 1
assert cass.play_count == 1
def assert_is_json(a_string):
try:
json.loads(a_string.decode('utf-8'))
except Exception:
assert False
assert True

View File

@@ -0,0 +1,35 @@
{
"version": 1,
"interactions":
[
{
"request": {
"body": null,
"headers": {
"accept": ["*/*"],
"accept-encoding": ["gzip, deflate, compress"],
"user-agent": ["python-requests/2.2.1 CPython/2.6.1 Darwin/10.8.0"]
},
"method": "GET",
"uri": "http://httpbin.org/ip"
},
"response": {
"status": {
"message": "OK",
"code": 200
},
"headers": {
"access-control-allow-origin": ["*"],
"content-type": ["application/json"],
"date": ["Mon, 21 Apr 2014 23:13:40 GMT"],
"server": ["gunicorn/0.17.4"],
"content-length": ["32"],
"connection": ["keep-alive"]
},
"body": {
"string": "{\n \"origin\": \"217.122.164.194\"\n}"
}
}
}
]
}

View File

@@ -0,0 +1,20 @@
version: 1
interactions:
- request:
body: null
headers:
accept: ['*/*']
accept-encoding: ['gzip, deflate, compress']
user-agent: ['python-requests/2.2.1 CPython/2.6.1 Darwin/10.8.0']
method: GET
uri: http://httpbin.org/ip
response:
body: {string: !!python/unicode "{\n \"origin\": \"217.122.164.194\"\n}"}
headers:
access-control-allow-origin: ['*']
content-type: [application/json]
date: ['Mon, 21 Apr 2014 23:06:09 GMT']
server: [gunicorn/0.17.4]
content-length: ['32']
connection: [keep-alive]
status: {code: 200, message: OK}

View File

@@ -0,0 +1 @@
This is not a cassette

View File

@@ -0,0 +1,34 @@
[
{
"request": {
"body": null,
"protocol": "http",
"method": "GET",
"headers": {
"accept-encoding": "gzip, deflate, compress",
"accept": "*/*",
"user-agent": "python-requests/2.2.1 CPython/2.6.1 Darwin/10.8.0"
},
"host": "httpbin.org",
"path": "/ip",
"port": 80
},
"response": {
"status": {
"message": "OK",
"code": 200
},
"headers": [
"access-control-allow-origin: *\r\n",
"content-type: application/json\r\n",
"date: Mon, 21 Apr 2014 23:13:40 GMT\r\n",
"server: gunicorn/0.17.4\r\n",
"content-length: 32\r\n",
"connection: keep-alive\r\n"
],
"body": {
"string": "{\n \"origin\": \"217.122.164.194\"\n}"
}
}
}
]

View File

@@ -0,0 +1,18 @@
- request: !!python/object:vcr.request.Request
body: null
headers: !!python/object/apply:__builtin__.frozenset
- - !!python/tuple [accept-encoding, 'gzip, deflate, compress']
- !!python/tuple [user-agent, python-requests/2.2.1 CPython/2.6.1 Darwin/10.8.0]
- !!python/tuple [accept, '*/*']
host: httpbin.org
method: GET
path: /ip
port: 80
protocol: http
response:
body: {string: !!python/unicode "{\n \"origin\": \"217.122.164.194\"\n}"}
headers: [!!python/unicode "access-control-allow-origin: *\r\n", !!python/unicode "content-type:
application/json\r\n", !!python/unicode "date: Mon, 21 Apr 2014 23:06:09 GMT\r\n",
!!python/unicode "server: gunicorn/0.17.4\r\n", !!python/unicode "content-length:
32\r\n", !!python/unicode "connection: keep-alive\r\n"]
status: {code: 200, message: OK}

View File

@@ -1,131 +1,146 @@
- request: !!python/object:vcr.request.Request
version: 1
interactions:
- request:
body: null
headers: !!python/object/apply:__builtin__.frozenset
- - !!python/tuple [Accept-Encoding, 'gzip, deflate, compress']
- !!python/tuple [User-Agent, vcrpy-test]
- !!python/tuple [Accept, '*/*']
host: moz.com
headers:
Accept: ['*/*']
Accept-Encoding: ['gzip, deflate, compress']
User-Agent: ['vcrpy-test']
method: GET
path: /
port: 80
protocol: http
uri: http://seomoz.org/
response:
body: {string: ''}
headers:
Location: ['http://moz.com/']
Server: ['BigIP']
Connection: ['Keep-Alive']
Content-Length: ['0']
status: {code: 301, message: Moved Permanently}
- request:
body: null
headers:
Accept: ['*/*']
Accept-Encoding: ['gzip, deflate, compress']
User-Agent: ['vcrpy-test']
method: GET
uri: http://moz.com/
response:
body:
string: !!binary |
H4sIAAAAAAAAA+1c6XLbxpb+ffUUHWZsyhWBxEqQsiiXLMmxM06sWLKdjMulagANAhaIhrGQknNT
Na8xrzdPMud0YyWpxbKT+XGvy5bARi/nnP7O2k3vfXf06vDs95NjEuTzaH9r7ztFeR/6JMrJi2Ni
f9gne/iCuBHNsmkv5srHDF4qIZvIX2P5y+6RiMazaY/FPRjz3XsWe6H/QVGaKcv54M9tU95prvEt
c90+ySwv58GGTYyuTKEo3WkCRr39LSAhD/OI7Z8ev5rzzyTMSMyX5Gf+eUBOuZ8vacoIjT1yyOfz
Ig7zK+LzlDxlec5S8jNNL1gexrMBUXDM3lBOtrW1tTdnOSVuQNOM5dNekfvKuLdfNgd5nijsUxEu
pr3flDcHCsye0Dx0ItYjLo9zFsOYF8dT5s3YjhukfM6m2sbhh7K3cnaVtMfm7DIfokge1zRIEsgQ
ZonC+IKkLJr2aAR8xDSHsTnMAA1JEoUu0MLjYZplP1zOI3iFXE17r09PiT5QeyRImT/tIRm7w6HP
mJcN8KdTpDFLBy6fD0GWTsRncjlBdEyBh94iZMuEp3mL1GXo5cFU01UVGMQNaXX3WOamYYLUtEa8
YxGswUjO5Ua9Y8RNGTBBYBdJVm7bDplX2wM7SKOrPHQzGMOjbEdsKY0ichHGXka4X02OcwYsSsgV
L0jEaBqTMHZ4Ad3nzWa/Y32ERZRB55KQPGBkyZx+RuY8y8kidFIK07k1bGANHoPgWTkRS7OBEA+C
RXJJstSd9oZDzx0HXpzNP9NkMR+4ES88PwX6BjHLhwBwlmdDOSIDQXuwgeHndPAxe2Lrjmt7zLap
5riMmTqzXG+sGyNv5Pu2DVp17noxoCMtWG9/r5xEwLWBRJZfRSwLGMurjb4jRWLcsAWggZtlTwxD
tRxfdcwRo75N2djwTUObODZjVB+rbo/MmRdSWBf2EHS1xKEAMIzvkCzE1UYvrMWUnBduoISwg0qS
MpB4wjPm9UgWfmZgDCz70rK/jJVwTmfAik8XOCuCWSmfFcseJPHsiW2YFrUskzqmMbKBPc03TXXs
6MbYMzRbXdOzu1Bq65e2/s0otXVBKbXGk4lD9Ymqe7qjjSdsNKGe6lmqRtWJ7t6LUk0zL+HfN6MV
5hLEjh2PTZAsylx/Yvi+Ri3DdI2JP9JVg92TWBOINb8hsaYkdqJabGR7NjVUUDTfZIAG6o9GFKge
u56Ea9eepdzhedYyZTHnXiJxDT0bxlwa8xjUKFoxtkAH2td6RNu0sjT0r5SF1p5e1yIeLn58bg1/
//1g5gQXv4W/zgM2fLe8LOzhZ//zyfPZs7NPHyN1uoHcGeczkG8W5kwR05eK3VrBPw/emma8PHHz
o0+/G0FyFVv6M684A8b+078owp+eW+Pxr78enr7ZsMA8W9Ao9MB0D9Q23aOD0Vg9MEaGNTGfHR2a
T/XJkaqZh7qlWiPj8Ki0BGKmJOUJS/MroMXZTWDXzkOvNdXYHE/AAuqmVQ4aSs+/tedw76qKGIQZ
xx8KOAR2SRzqXsxStPvAfQS8t1tiHmZgP5EXL1yQ0MON5ejT6vCDwxA/4kvRC+McWBMmKTu05nKi
gimw2wsKnoNmOXZUkHgKriJtFilH1m/qztUKrU5pvW6LQvDGvKYwS2hsEBcEVK5RdqaVjrQaoTmc
z77EN5W6gzqDq5bKolrGxGMeKLfPxp5tUt+YOJbh+eCzfDCYEIpMe+DPe0RGBD1NR51l4SyAF6a2
6glq6oa0ZnYI3NYfYrpo86vpHabKTkUTNEJ3+KcgqLrsR2HVBxRhTlamqYXWVdEh4NIrXNT2nDoC
VcBSb/+kbG6RLUmPwm+wqAhZhhnj7VWB8ZfY/tcsWYc47SWN3n4dMf81y8rwslnR7O0/haa/SK4U
YsC8vZzV2z/AthuX2xsW0QbMtVS1ApwC4U4KtqyIIiVFxEvN76BThsdbG7hotL0i//ta1XM+Qxue
8KRI2gyMIABsJgG7QoRz+1SwDE08BqkgnOcQC+8Nw/22kiH5KceEYM7iol5IrEDET0WE0DQFQwTO
OWlbmBbvIu7ralrrrRBFxPx8dYc6RCspn7FUrIcBbXer29Zg4+IKuoDV+ZP9vSwHkzbbvw4McrUz
iPhRPOR54aB4IJyWwyAXPAbzf5UHmHpgIhFDcoRJwgXmlgJKInHZGyY30Lv6MUibLU1UAoFEzuda
W7bl+r+WW5gRgVDyokxg6mz1SU1qM7YLVMDX/tsQ/L7Ia25UwX3ghKDmoQxEcjwrQsjbIMdKIeOK
WJltcZgp7aZAKJ6UZbxIXZYNNmjqHWiorc/wU2//14d0njw+EJQ842kxR6FDZuiRT7VMlmEeAEHZ
EhIwAq5rDgmeV4A8rgi7xEBilZK2Erf2pOm0alIysLVucB+dtDfrpJwQtPFUPNxDH8sZNmokbNlc
RAgyRin7Kti8EgXECaBJpmclj2UYJz+dQzABkWkSUZcFPIK5pj1J8WAwqMkSsyjZHLLv7vQZi5ib
r8woqxo3jxSjuSgSEAgnC6xiYJcDyO8h6xZ1Gfn6llFSrxudvtOgT7SFu/URoGqCr06bU4DuxnU0
KD/JXyIgrJ5LXn/ke0PZ0kYlbtCNuKzVqOsqrg0d+CyMe5uJKt81SB1DSMFnYFqk5WuWlNrSDcSG
4Lwq4sr28kEG4ywt32YgqLAlmM1hcolTBkalFcFujpKXATBd2kqrE9UXsVOkWb45fCb5kkHU342i
W7HkCIDlQ6xrtBUpMOsumCvBajGSIGkk6A1IxiHbAaOUWCVN6hdEO0NIxzznCvIxxjG2BrhGXJlj
xFytG4lKHo/dKHQvpr3zGf00SIos2H7fP4f4wr0AxxTn/R3Sx1wHkyV8xloLwdyzRBm2oVU/iGOQ
lcvmOObDo8crtK6XTDu4p22wBmZbUFptCFEoco9cmmSVUIjwcEJYKzYCZF+nbLqU6QVLY8j20R+n
DKmhxJHl2SWF2BOH7IPKpe2JwC14vPFCbUK1NqFGh9CczoT3Auq0ClMd+n4HV0ZccCYgXPAu4AVZ
ugCa8o5kSt/7jjWVRiAGOm1wy8PA2JzcrACyndvcM11DRAxlOKWrqkjaHM/XLM90bI1NTGPkau7E
sU1n4mu+6088s0zaXuMgBIDTSoLFTCgqs0noDKNJ6LSJvTmj69iyVXNRWgh4bAtAVA6zgOcdg/AV
YmhmlNU+a2KbJnOYpdm6M3YmGiSv/kijk4nm+Z7RJK+gM1Wl+bSeoxGAqtmNBAxTX5VAxedGW9bm
DcDfRmZtTF7EucgvMbTqkgM6QQFhgS7Hm53x0mCh1kE8t6FqXtXUUXHm0Dxjonp+hXiv6uPMB48k
oiep7JsrEqvANdcsaVOECOMFWIZhFs5ijJMqgeS0ckszlDAQAOMVYcTATLoXX24AG7P3molIERwb
Ln3+/ETavapZKLh4J+pgtZFrY3aFwSZC3wssIorkEKjRFBwqRmK7mpVcwgqvUpCbdets5gYxNYWG
v0NCZ/QCNp+cgSRq8Yi2A9FGXvkEM6Pq8GxdRM1D0kpozJWE5iXnF4hBTChOXr96gqIHHwFQzUNA
Xof7u7EDswj6UQovwdnhh+dY9js8O5Bs5JI1H2RGDFXx6BXJ05BGBD0LMjIQ5Cd1fHNDwFJWDYX9
uz486VTg2l0WkELxDnI6oUXH8a2X777H8EBOQTyaU2XuQw6AprD9ppyRJyyWqYgi6s/3DCIkQoZi
bvz8Fh9qhLScUyd9FyXXklmR17T99JLmkLQgqMCjple1H8SIc01X1l3E1tr+rAq+HYlZa9sk4kCe
VJZpr8xn2+rYuP9NfImzRoKiUzy+jDsFivZCDt9QcL6maIIUd2OirisAZYRMFmVW23GU6RmSUpv/
emyyLxzEZ1ZbcYQAnpJK246V/x3ihZmLBW0SgEREe3VaCtFWFoCWQ0wbAPGzADyFixojyisy8Z9z
8Bsi68aRQrmEYAZktf7R+XR9LCxGX2vsvsq24cw1Zo8vkwhpR7LFm7VaX4vglYynFcKUsLkviKqN
+bthJCrGVe0IIdRcdtgAo+YIvqr+CKXOWhDAQ3JGQaEx4F07VBf9LhiT8XCR4JmQR7ACUlVnPLZg
EU8wGcnWSmd3hE6rPP4XwOd1VcvaCKH67f8LjOAB0JCJsOVvx9JPHLOngCGMDluEbEJS1Rc8sLqj
quoPYEvmDqZTy4ALw5OGIHKBJxTr2k0LCTl5d2NjfQ8axMiyaoMhRjG/L6Japx9/AaLqM5SNiLr+
hOVLEbXBedYHh+sRA0YTAbR0j0CXIYRm9JyZ3kIzLiFvrPtXb2Ab8VRehsAiJdodjycQ/sqMaNdS
VYyFZcgillTKxAm6dZqrFMrCS0OSgpzN8SS4Cp1EA7ithIknWbUs9y5zAzanA57OhiJKeeV8BLZh
InGe3MzkFenqcffJmfazZZ2KXG2ldx4UcyemYfQmjVojqjVlzQayvYEUh0CPx6IQD9Yh72QjT3MM
3aaW4410b6yPVENlhu5alm34Y9c14KU68QYOVuLWly8XfPP65T0WH+kmMzTVcXxfHauab3q25xuO
SUeWQy3mujbzJ77uXre42NovWNoHYARDMeo8iegVS88X+kAdZEv/ia5qhqJaimY+lKWUQw6In+r6
ZOI5DwMPxPvAegpR/APraOpHi7pJggJbbV2tW3HnsS3w2n0L/AmNQOQD4+CB/gz+biQV2hs54QfP
HDsgJG1ELdP2PcszmGG61J64rqMbhqPCvrnaGCVVryZQjOtp+lh9KGKjoxJbU02zBurDDEKrtzwq
5myKtYCHIsuBwfeg8M5A+pjMHhjPRNnj3IVtPE8Z3pjxHhhHoG+XoFsPsUjq+5D9eKeMXUx9GmXs
oVCw+9H2tTgrwPhR70gkKDXSKsDo2pmu7arGrjn+LzGYC73ebJzOmyAA37sR/No90u3Do6ejY+Xg
eHSkaJrrK5PR07FimqZlGZYJ7kitDRhExQjdXZFRd01YwiFwhs3dTRkkU8D+47a1A0OT0JTOqzsw
fBECO9UZxNerDTp1sUB3HRpFfOkXUVRdtCtXrC4DXjdEXhKkLkQuWa85YFnSq+yaYc7MRZWtO3+v
ij/X9F7iFca6L0/op2vpEfwvaNrQ8W8L8a9sIRqUCJ2Q1d5voUFkRVmqCu2aQtSaQErQT0uwkxqr
/wbpvzZIKzO35oHwev7N3kRT1QfXOhPxctO1/UsFtsG9WNIFUwQKe0TY2LZxFRoAv6WL3N+LeXkF
vRX9tu7ei0seZS2gn5FTLMftDatB5bFF+2edI1aXI899+IEp5GrBrp1CVH1kgiI/VUlE+QlSqqvr
C6p3uQy5sZpOOncfVxJovMSoyIXFZYLu9YubjuXsDXcsmztdEUACUut5Aok8XgRrPXYkUY2860m+
uGE0FELBtOZQPpA32eph/ZdNK+tu+69ZeZukrondf0qw3jlsG8x6Uj595XzlUciQJmFv/+DkxVdy
zNJ5piRpuKAubP4ZfiSiakBOZOP6/N07f50zDn3joX91Htw+8WjKYrCLWRF1b22tHGajbnqcZfH/
/vf/5HgdGi1ba+AOliPEGUYfD8cRdXgv5g5XqsRl+jmLPTxmaH3AYgugO5S1LFGY8fh3LREkmyRQ
3mY55MmVuOJIHkJ6fvWYoN8tLxPsgJFxBwRv7rzGLhlWzfAM3WvXZ25UOaM8UOzcjEuaulZ5yFRL
neV9PBwlPtj22Mu+a6Tc4WL9nrBUUUUWvG+6MHzLFY8bvz+1aorkakqaoSKenq7YIimcW+7/3EhM
vgzx6kRFw3Xrl91AJeTDN6djuVwOfOoyB5TiNmKqfr39Z+XTX0IOnoxAyBMLcrAummRPhKWdCosw
AwemT+yRqU7E5zy9mNKY5efF7HyWJnhMeQ391cS9/ZfiCS9VfUv6M2AgiYpsIL/OIejXNN0yTdW2
VFM1rZFqjkfGqCe/e5IUDtiIQDjrjQTLeXBKvJuGzz98c4knIfpNluWCXBmVrXvmkqC6M/iR6vFW
iq6917l6oCgVfSVk+Zpv7H3G7+r5I9+zVXfsqaZDPW9sUtvV7Inpj0Y2ZZiDbP6uXrWy/HKr+Jrc
R7qgshWviP8szHjsh7MBFpSzit8peY+13HNZH/3wuN3RYwka9tgFky460lyW2SH4O3fRMbHeTnlB
tB5apCkWHME+k2aNP/58vLX1H9vYIQQf8qj+BBFoAeCbhZ/nHFvb3z5svgHZ+vJflysCGX9KsIAO
i4hf//wneQ+kENKpqoOoD1wX4lRRVX9zoGhjWx3ZE0XD+vmG3kd8DqHjLxCj44DSB17T9wDTP6Gk
KXTGnbmh30HsBrzVT1zd39D5UGSHbyl2tXb6KE/xbd7+Tn+B15/FHLpcaKu8/78yC+DnxSzmkN+8
Zj6yIfUFK9zIydqqa/1bbN/WuY/3BfBcmMmTiLRaZXWcON84AQTid35lh5KDLdzObb+IxVnD9iPy
h2AKN3hGYXs97hZ4zDeQR4rHkbiBuN2XaOgDjTM6QKRA3/4KVvriJc2uYhfeougfi8mhEXQVmrb7
0ib2ybS1EmRd8kQK7HTOIZMnT0i/Mp5ZFvXJrvwsnUH/EfmB9EuDqtT3pKRroKDe/cc1S1mboxnL
S3ayp1dndIa4axh7r354TDJQW1SsXyBlG+DNnTR/yiAoZNszukMyIeg/H22v65CwXrfoESHUS3kU
wa4uzkMPSOv98sYePTdenh0//fHVyeHLZy/Md89Hx73HTd8kvCz7Hh29+OnFy59fHr89enr26vlv
1osf3xi66FvvJyk3FHnnkcdjLNnC4GUYe3w5kJ9xRKcBOjSAkHgg5+clAdiBedNmN6Vg3fR6sPRK
lh+1BgT4zWxAwHbpFns3Q+ARYKB2oNlA0iK/fblLKj9F2+3VYrD2AM1AaURhhwUgxV1b4KHf7lfi
eG2v2lMJ4ArqfyC94cehOJSFuRNAWtVxe/sWkKH1Btyi2YyLKPqAaINPpT25O0Jb8Hw0oAk6jsMg
jLxt6FMxFvrb9dY/+qN+3H70558CvtuPRM8Gv//4Bw4TxmFviN/KEXfDxX9qsfV/BBAejOZCAAA=
headers: {accept-ranges: bytes, age: '3593', cache-control: 'no-cache, must-revalidate,
s-maxage=3600', connection: keep-alive, content-encoding: gzip, content-length: '5300',
content-type: text/html, date: 'Sat, 10 Aug 2013 23:33:35 GMT', expires: 'Fri,
15 Oct 2004 12:00:00 GMT', server: nginx, server-name: dalmozwww02.dal.moz.com,
vary: Accept-Encoding, via: 1.1 varnish, x-varnish: 2027569899 2027508286}
H4sIAAAAAAAAA+08a3PbOJKfV78Co7nETo1I8SlKjuWUXxlnzpl4EieZuVTKBZKgyJgiGZKSbM9O
1f2N+3v3S64bIClQkh3ntXu1tUlskiDQ3Wj0G2B2fzh6cXj+x9kxCctpvNfZ/UFR3kUBiUvy7Jg4
7/fILr4gXkyLYtxNUuVDAS+ViI3EZSguTpfENJmMuyzpwpgf3rHEj4L3irIEWcGDP58CeS9Yw0/A
+jSQSVnBwYZNE10BoShtMCGj/l4HSCijMmZ7r45fTNMbEhUkSRfkeXqjkldpUC5ozghNfHKYTqez
JCqvSZDm5ICVJcvJc5pfsjJKJipRcMxuXwDrdDq7U1ZS4oU0L1g57s7KQBl296rmsCwzhX2cRfNx
93fl9b4C0DNaRm7MusRLk5IlMObZ8Zj5E9bzwjydsrG+cfih6K2cX2fy2JJdlX1kyeOGBkEC6QOU
OEouSc7icZfGMI+EljC2BAjQkGVx5AEtadLPi+Knq2kMr3BW4+7LV6+IoWpdEuYsGHeRjJ1+P2DM
L1T87c7yhOWql077wEs3TicCHSc6oTCH7jxiiyzNS4nUReSX4Vg3NA0miAsidfdZ4eVRhtRII96y
GHAwUqZiod4y4uUMJkFgFUlRLVuPTOvlgRWk8XUZeQWMSeOix5eUxjG5jBK/IGlQA0eYIYszcp3O
SMxonpAocdMZdJ8uF/st20KxiAvoXBFShowsmLtVkGlalGQeuTkFcF4jNoAjTYDxrALE8kLl7EFh
EbMkRe6Nu/2+7w1DPymmNzSbT1UvTmd+kAN9asLKPgg4K4u+GFEAo31YwOgmVz8UT1zfGHjGcDhy
mD/Qh5pv6czTKdNt2wsM3eqSC89PQDryGevu7VZAgARJIoryOmZFyFhZr/M9CeLj+pL8qF5RPPFH
pu/pTmDpph4wx3YcNhwarm0OXMv0NKOWOy6wMKBFomCPLK4AnSllOvNCJYIlU7KcAYuztGB+lxTR
DQPtt50r2/k84qMpnQDxAZ0jVJRepbpXbEfNkskTx7RsatsWBbIHTqC5emBZ2tA1zKFv6o62plj3
odQxrhzjm1HqGJxSag9HI5caI83wDVcfjthgRH3NtzWdaiPD+yJKdd260lGAvhGtAIsTO3R9NkKy
KPOCkRkEOrVNyzNHwcDQTPaFxFpArPUNibUEsSPNZgPHd6ipGcwOLAbSQIPBgALVQ88X8to2YHnq
pmUh2a4kTf1MCDb0XE7Mo0magOLEK9YV6ECD2oyQbSnLo+BamesyeEOP02j+84nd/+OP/YkbXv4e
/TYNWf/t4mrm9G+Cm7OTydPzjx9ibbyB3EmaToC/RVQyhYOvVFnCEFyEbywrWZx55dHHP8wwu05s
46k/O4eJ/WdwOYt+ObGHw99+O3z1egOCaTGnceSDrVY1me7B/mCo7ZsD0x5ZT48OrQNjdKTp1qFh
a/bAPDwS8xeQsjzNWF5eg5FYROiHd6jngYkuLyJfAqnbAxsUU5N80HJk4O5ksN7tEUNrODLMgWHZ
Fbq+CBI6u27qX9fBBbf4+EsB38GuiEu9y0mOLgL4FgPX5JYkjQowtcgFP5qTyEeRSNH9NZFKCkOC
OF3wXhgSAU4AUnWQYLnxjCkgJ3MKToYWJXZUkHgKXiVfIqlGNm+azjUGqVPe4JUoBMedNhQWGU1M
4gGDKhxVZ1prl9QIzcV8QoRH74JLB2Fm0SQE3prAUwg2xl3w2BjIQLgTkiCKQfR/DIIA3AD4ywJi
qymgBDVg2/qjLgHIQM9zXTNUs2cY6vAUgPYs1TgZmurAUzTV6WmKDi81dcCv8BNrPVMdHjpDdQQ3
Ts/BwVpvYKmDnuYpuoZDhtA2UC1FH6nDHhgjh0/iDYAOlYFqQjfVhn4GwAAkiom9VOcUaLB6MMY+
1TVV79nqCHrCX0ABI3g/vJ5oc8MBUI5qeZxGpAuoHfAr/ISaB2QAAoTCfwv8ugP060i1bld3Cr8D
CDB9HITgBKD6B9G8MTSgGonGmdWT6jXT83QdMfWgm644MCt+A+hsjthDLg6ADhOmYMB1qFjqMNQH
p0OcsamrVqhDg2nhjHrNX/ke57R5rnPAaJ7wVbwhz/lCGAPVEJgVm3Na1wDzCC9DoFZD9LDQNj71
qkbeER5gVUd4gbZDhwPDFXaQlQi21yC4EbrfB6GUJLdPG4nvg8g3Dwmdy0KvGy3JrjrNlkkGdIcf
BS1LWwfiqO4DdnRKVsA0mtO28H0wTv7MQ2dRUpebFlCh7t5Z1SyRLUiPo2+AlIe4/YKlMlaY+Cm2
fx+UTUgsozS7e02G9X3QinRkidHq7h1A03fiK4WcoZTR2d29fWy7E91ufxZvkDnJXtcCp0C4nIND
m8WxkqOFFea/JZ0inepsmMXS5Nfk/9jY+zKdYAiQpdkskycwgIRhCQScC+Gx0ccZKzBCwKQGmHMC
udNuP9qTlQzJz1NMIKcsmTWIOAbCfys85aI5eCOI7TLZzUhznzI/WtE06S1nRcyCcnWFWkQreTph
OceHCVB7qWVrsBG5gnHAKvxsb7coIZqc7N0mDALbOWSIyB5yMnORPWCWxDCikGOIAa7LEFNVTDwT
SKYxqbzEWgQXJZ7o7vazO+hdfQzz5ZJmGoE4tEynuszbCv9v1RIWhEsoeVYlvE1140lD6nJsW1BB
vvbeRBA28jz4ThXcg5kQ1DzkAS+mTGYR5PmQk+eQocesys5TgJS3U2ZkT86KdJZ7rFA3aOo9aGis
T/9jd++3h3SaPd7nlDxN89kUmZ5DpEQ+NjyBEDMEgooFJOwEsoYpAYWYAT+uCbvCaHKVElmJpTVZ
dlo1KQXYWi/8Ep10NuukAAja+IrffIE+VhA2aiTGaDxMFIFq1VfB5rZyRkkG0iTS+2qOVRYgni4g
ooTEJoupx8I0BljjrqBYVdWGLA5FKaY0jlciTRYzr1yBKKpgd4/ko1NeVCKQjcyw6oVd9uMYqzS8
jidef2KU0OulTt9r0Ecqyd36CFA1Pq9WmzsD3U2alEA8iQvPCur7aq4/p7t90SJLJS7QnXLZqFHb
VdwaOqSTKOluJqp6t5TUIYQU6QRMi7B8S5RCW9qBWB+cV01c1V7diIyM5dXbAhgVSYzZnCtVcsrA
qEhpzOZUaRHCpCtbabdSu1nizvKi3JxDkXLBIPVrp1JSLDkAwQoKVpqyIoVW0wVTbcCWIAmCRoLe
gBQpJMtglDK7okn7jGinD9m8715DOs9SLGeAuMapAnfL3C7mld808eLIuxx3Lyb0o5rNinD73dYF
xBfeJTimpNzqkS1MeDFjxnus1REsXVRShm1o1feTBHjlsSmOef/o8Qqt6yX2ltxTWVhDS2aU3hhC
ZIpYI49mRc0Uwj0cZ9aKjQDeN3m7IXh6yfKE+dwf5wypocQV5fwFhdgTh+yByuUyIHALfrr0QjKh
ukyo2SK0pBPuvYA6vZapFn1/gCsjHjgTYC54F/CCLJ8DTWWLM5XvfcuWlWkgBjptcMv90Nyc3KwI
pJzb7EbTyeeUnqtKGUpEX4RThqbxApnrB7rtW66js5FlDjzdG7mO5Y4CPfCCkW9VJYCXOAgFwJUq
IRwSsgp61QUE01wWEPSRs1obXpvnurmoLATcygzwcsaSIkzLlkH4CjYsIYpisT1yLIu5zNYdwx26
I50GZjDQ6Wik+4FvLkshoDP1zsSrBsaSAZruSCUUy1jlQD3PjbZMnhsIvyyZjTF5lpQ8v8TQqkUO
CFdoiKFWa6iwVahwEMpt2GCpt19QZ6bQPGF8o+UaRb3eSmEBOCMeOAk931yRWpVZa82ILotQXsi8
Swhe+wEwscwjrKPWDClp7ZYmyGGgAoAo3IiBmfQuP98ALs3e0xrdxcmZMHnn9BJmTPAFMTXliF6T
c+zQGDhZXldmuIzOd0Ob8B0VCNJoDs4Uo7Ad3c6uAMWLHBhnfxKatYFPyyLDP4I7FS/OYe3b/Nnn
beRFQDArqjda11nU6LG43O7vq8orNx+3e/dWFVPuMocMJG0xv+WZW35jvQT6I3pXAYL4FJLzaQAh
NFoS+U0FMc1YIiJ5hVf/v9AHCyb3OWx8foM3DZMl297KfnnZuposTwtkN7egJcT8uC7gkPLrxo1g
wLYmbusWtrO2PquMlwMZe22ZeBiVZrV271bpoCzRS++5aV58a5cg6xQ/XSSt/F5G5KYbiva31ByQ
4nZI0bakIM+QCCLPGluIPD1HUhoT2ozN9rh9vWGNJUQRwE1pYR9x36VH/KjwcFOAhMAR3l5vTkOw
UoSgKBAShkD8JARr64FtIbw6IfLmaQq2lyetOBLNodjzVslq+aD1dHsoyUffai++yjwg5EZmj6+y
GGlHsvmbtVKZRPBKwiBFAJXYfKkQ1QvzjxYjXnCtSy8oQsuzJRvEaHnioS6ecKUuJBHAMwmMgkJj
vLh2hoH3u2RMhJOzDHfkfIIFhLq44bM5i9MMY/lirfJ0T9GRqsvfQXxe1qWgjSLUvP2niBHcgDQU
fPP0Hy5Lv6SYfIQMxehQImSTJNV9IVbRepqm/QS2ZOpiNrIIU2548ghYzuUJ2bp2sEWInDgqs7E8
Bg18ZFX0wJLfbPqlEiVtHnwHiWq2IDZK1O0bFJ8rURucZ7P5uh4xYDQRQkt7G3kRFWVEL5jlz3Xz
CtKupn/9BpbR73aqKJJnFDvD4QgiSJFQ7NiaxsNJAbJkU9wer2Mh3gB+KGP8TlTxOtVqFBBuT6ma
5pM+jzteuB9gIgCJ77IvQWFtTtpfxwSjsnBbRecVBhk8iVkZ5c/y1UMHZ+f6c9t+1fTuLHuX4Wzq
JjSKX+exNKIhVdQ+IGtSBV+4GPksjvB8A+RvbODrrmk41Hb9geEPjYFmasw0PNt2zGDoeSa81Ea+
6oJqddaprTC+fnn6JdgHhsVMXXPdINCGmh5YvuMHpmvRge1Sm3mew4JRYHi3YeeL/Dm4A5CRsM+H
XWQxvWb5xdxQNbVYBE8MTTcVzVE08yGdlekZvB4HNC7YQ1Gaqh64UQD/jMV2N2YvktOU+mNMSR+K
WsZhCjozdlxnNKAPWeJzGTlgIZ1H0O6zgM7i8mEwi2ORNx9FBQVAFYzQh5V8YB9ABvLAPhoH8bxp
EoKLrY6hNa0om9gW+nLfGf6GRmDGA3P/gfEU/m1kCbQvFwQffGvowmroA2pbTuDbvslMy6POyPNc
wzRdDSTE04e4JA02rl6ITzeG2kMejh1VYjzWdVvVHiKzD7ilqfgmZgv5/uKsedVuc2m+bHiTxrNp
PaiMYpT39uz4ot4xsXtL+ods8sB8yusbFx6I2UXO8GSV/8A8AgNyBXbjIVZDg4BBKPqKsctKMLjl
WCPrXkz/Wj2YgZmm/hFPpRpNEPJsK4Z+bug7mrljDf+Lm5CU26vORjN6geFKh5tSfO/FcNk5MpzD
o4PBsbJ/PDhSdN0LlNHgYKhYlmXbJjAFPGdjaiF+x8Xe4elz29hmKYT4IBM7OYO0D6b/WLbLYEAz
mtNppzorlc4jmE61b/At1BrjD46hjYjGcbpYqmODsj4mKg8h0hBxfJR6EGQV3U6zlbKg18Utw9yJ
h7ahwfCjxv/cQtcCD7c2fdOMfryVHj7/Oc0lOv4FLVjnFm3q/IvYsM66EVub2j/Fit3G+K8yY0tJ
5pPu8IrRt9DyzopC1/XiNaVttJVUijmuFLLT6NO/FenfivT/XZFqd7HmyvEDmLvdsq5pD271yvxl
t7P+YcyVAovgXS7onClcU7od7qtkJ8X5D1cRa+ztJmn1kYeU6Ehft+CxmE6THBGeHO3260HVRo/8
uykL1GeKLwL4hVWD1RqtnDXWfUROKp7qvLF6giz6+vYa+n3OEG/cgyCtI8MrNRM8cawIxPz4RfvA
yl0bmc6Go8nLU3AxiIRSRNMsZvyspnTb4kQ98r5nH/iZrD5nCua9h+KGvC5Wjzd8HlhRat17yarz
N00Z9MtBgocpYdkA6ll195Xwqg2kPs2i7t7+2bOvnDHLp4WS5dGcerD45/hIeKGInInGdfjtU5K7
mVQYMzYek6h30E/T9BJrn3jobFkJhVUswAm1zrmtbP9j4cJPWZH873//T4lfEaBpkwb2sAJF8DzB
Fh4nQKnDk0T3OITGv16ZgmPEnSXpAetrIN2RKF/yWpyf/iCxINvEger8z2GaXfNDoQQcdHb9mEBs
YFXHL3rkWeKpBM86vcQuBRZK8dSBL5fk7lQ5s9qHbZ0lzJalzOqcYcN1VgJXXEYCsO2JX/yw5HJr
Fusnq4WKKmKP464j1p84FHPnF4qrpkhgU/ICFfHVqxVbJJjziRNTdxJTfbNS03Ab/qobqIS4+eZ0
LBYLNaAec0EpPkVM3a+797S6+y7k4GYYBEsJJwdL4VnxhFvaMbcIE3BgxsgZWNqIP5f55ZgmrLyY
TS4meQZqeRv9NeDu3im/w2No35L+AiaQxbNCFd9Pcfp13bAtS3NszdIse6BZw4E56IqPvbKZCzYi
5M56I8ECDoLE03x4/9M353gWod9kRVmvfvhx3TFX9DR9wY3Ut58k6NaDsKtbyELPVyKWr/kk9gY/
hmWWMRyZ1PXoIDCCoRMYrm1rBmVDI9BHrnPbx7A1ZvH1OP8u9QOdU9GKZ+qfcyueBNFExS2Eop7v
mLzD6v2FKKC/fyx39FmGdj3xwKLzjrQUGysQ+1146JdYt1edqG2GzvIcK8tgnskSx59/Pe50/mMb
O0TgQh41TxCAzkD2JtHNNIVW6ete6Qtj6WPb9qQIgWyP4I4J4OCXv/+dvANKCGltowCn98XXdrhh
8npf0YeONnBGio4bJht6H6VTCBx/hQgdB1Qe8Ja++5igchXNoTMuzB399hMvTKV+/FOHDZ0PeSL6
hmJXu7eF7ORfy2/1tuZ4XJzDMASiTvW9xAoU6vvPJkkK6c1LFuA0RA6DGyA4kzWsa/2laX+q8xYe
EMGDAExsPeU1ltVxfEPrDAQQv6kXHaoZdHA5t4NZwjeXth+RP/mkcIEnFJbXT70Z7uuqYg/5OOYn
Nre3hDRsAY0TqqKkQN+tFVnZ4i9pcZ148BZZ/5gDh0ZQVWja3hIWcYuMJUyQc4ktSLDSZeqlMXlC
tmrTWRTxFtkRz8IVbD0iP5GtypwqzeEy4RgoaPfW42ZKhTyjCSur6RQH1+d0gnK3nNg77f1jUoDW
ol79Cgmbiqed8vKAQUjItie0RwrO6L8ebaNitXWIG69P6BEh1M/TOIZVnV9EPpDW/fW1MzgxT8+P
D35+cXZ4+vSZ9fZkcNx9vOybRVdV36OjZ788O31+evzm6OD8xcnv9rOfX5sG79usJ6kWFOeexn6a
YOUbBi+ixE8XqnjGEa0G6LAUCCEP5OKiIgA7VDUXwVfBWC+/XVi61ZQfSQNC/J8PQAK2K6fYvVsE
HoEMNO6zUAUt4mPnHVJ7KSq318gAt4pmoLKhsMJcIPnZZJjDltyvkuO1tZJBccHl1P9Euv0Pfb4L
D7AzkLS64/b2J4QMjTfILZrNZBbH71Ha4KmyJ/eXUEk8H6k0Q79xGEaxvw196olFwXaz9I/+bG63
H/31Fxff7Ue851J+//Y3HMaNw24fv2LiZ+n5fxrT+T8T/YhcRkYAAA==
headers:
Server: [nginx]
Content-Type: [text/html]
Vary: [Accept-Encoding]
Cache-Control: [no-cache]
must-revalidate: ['s-maxage=3600']
Expires: ['Fri 15 Oct 2004 12:00:00 GMT']
Server-Name: ['dalmozwww01.dal.moz.com']
Content-Encoding: [gzip]
Content-Length: ['5683']
Accept-Ranges: [bytes]
Date: ['Sat, 11 Jan 2014 18:45:11 GMT']
X-Varnish: [918768771 918700396]
Age: ['3479']
Via: ['1.1 varnish']
Connection: [keep-alive]
status: {code: 200, message: OK}
- request: !!python/object:vcr.request.Request
body: null
headers: !!python/object/apply:__builtin__.frozenset
- - !!python/tuple [Accept-Encoding, 'gzip, deflate, compress']
- !!python/tuple [User-Agent, vcrpy-test]
- !!python/tuple [Accept, '*/*']
host: seomoz.org
method: GET
path: /
port: 80
protocol: http
response:
body: {string: "<html>\r\n<head><title>301 Moved Permanently</title></head>\r\n<body
bgcolor=\"white\">\r\n<center><h1>301 Moved Permanently</h1></center>\r\n<hr><center>nginx</center>\r\n</body>\r\n</html>\r\n"}
headers: {accept-ranges: bytes, age: '0', connection: keep-alive, content-length: '178',
content-type: text/html, date: 'Sat, 10 Aug 2013 23:33:35 GMT', location: 'http://moz.com/',
server: nginx, server-name: dalmozwww01.dal.moz.com, via: 1.1 varnish, x-varnish: '2027569892'}
status: {code: 301, message: Moved Permanently}

View File

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

View File

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

View File

@@ -0,0 +1,129 @@
import pytest
aiohttp = pytest.importorskip("aiohttp")
import asyncio # noqa: E402
import contextlib # noqa: E402
import pytest # noqa: E402
import vcr # noqa: E402
from .aiohttp_utils import aiohttp_request # noqa: E402
try:
from .async_def import test_http # noqa: F401
except SyntaxError:
pass
def run_in_loop(fn):
with contextlib.closing(asyncio.new_event_loop()) as loop:
asyncio.set_event_loop(loop)
task = loop.create_task(fn(loop))
return loop.run_until_complete(task)
def request(method, url, as_text=True, **kwargs):
def run(loop):
return aiohttp_request(loop, method, url, as_text, **kwargs)
return run_in_loop(run)
def get(url, as_text=True, **kwargs):
return request('GET', url, as_text, **kwargs)
def post(url, as_text=True, **kwargs):
return request('POST', url, as_text, **kwargs)
@pytest.fixture(params=["https", "http"])
def scheme(request):
'''Fixture that returns both http and https.'''
return request.param
def test_status(tmpdir, scheme):
url = scheme + '://httpbin.org'
with vcr.use_cassette(str(tmpdir.join('status.yaml'))):
response, _ = get(url)
with vcr.use_cassette(str(tmpdir.join('status.yaml'))) as cassette:
cassette_response, _ = get(url)
assert cassette_response.status == response.status
assert cassette.play_count == 1
def test_headers(tmpdir, scheme):
url = scheme + '://httpbin.org'
with vcr.use_cassette(str(tmpdir.join('headers.yaml'))):
response, _ = get(url)
with vcr.use_cassette(str(tmpdir.join('headers.yaml'))) as cassette:
cassette_response, _ = get(url)
assert cassette_response.headers == response.headers
assert cassette.play_count == 1
def test_text(tmpdir, scheme):
url = scheme + '://httpbin.org'
with vcr.use_cassette(str(tmpdir.join('text.yaml'))):
_, response_text = get(url)
with vcr.use_cassette(str(tmpdir.join('text.yaml'))) as cassette:
_, cassette_response_text = get(url)
assert cassette_response_text == response_text
assert cassette.play_count == 1
def test_json(tmpdir, scheme):
url = scheme + '://httpbin.org/get'
with vcr.use_cassette(str(tmpdir.join('json.yaml'))):
_, response_json = get(url, as_text=False)
with vcr.use_cassette(str(tmpdir.join('json.yaml'))) as cassette:
_, cassette_response_json = get(url, as_text=False)
assert cassette_response_json == response_json
assert cassette.play_count == 1
def test_post(tmpdir, scheme):
data = {'key1': 'value1', 'key2': 'value2'}
url = scheme + '://httpbin.org/post'
with vcr.use_cassette(str(tmpdir.join('post.yaml'))):
_, response_json = post(url, data=data)
with vcr.use_cassette(str(tmpdir.join('post.yaml'))) as cassette:
_, cassette_response_json = post(url, data=data)
assert cassette_response_json == response_json
assert cassette.play_count == 1
def test_params(tmpdir, scheme):
url = scheme + '://httpbin.org/get'
params = {'a': 1, 'b': False, 'c': 'c'}
with vcr.use_cassette(str(tmpdir.join('get.yaml'))) as cassette:
_, response_json = get(url, as_text=False, params=params)
with vcr.use_cassette(str(tmpdir.join('get.yaml'))) as cassette:
_, cassette_response_json = get(url, as_text=False, params=params)
assert cassette_response_json == response_json
assert cassette.play_count == 1
def test_params_same_url_distinct_params(tmpdir, scheme):
url = scheme + '://httpbin.org/get'
params = {'a': 1, 'b': False, 'c': 'c'}
with vcr.use_cassette(str(tmpdir.join('get.yaml'))) as cassette:
_, response_json = get(url, as_text=False, params=params)
with vcr.use_cassette(str(tmpdir.join('get.yaml'))) as cassette:
_, cassette_response_json = get(url, as_text=False, params=params)
assert cassette_response_json == response_json
assert cassette.play_count == 1
other_params = {'other': 'params'}
with vcr.use_cassette(str(tmpdir.join('get.yaml'))) as cassette:
response, cassette_response_text = get(url, as_text=True, params=other_params)
assert 'No match for the request' in cassette_response_text
assert response.status == 599

View File

@@ -1,82 +1,71 @@
'''Basic tests about cassettes'''
# coding=utf-8
# -*- coding: utf-8 -*-
'''Basic tests for cassettes'''
# External imports
import os
import urllib2
from six.moves.urllib.request import urlopen
# Internal imports
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'))):
urllib2.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:
urllib2.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
urllib2.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 = urllib2.urlopen(
'http://www.iana.org/domains/reserved'
).read()
assert '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 = urllib2.urlopen('http://httpbin.org/').read()
assert 'difficult sometimes' in response
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 = urllib2.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 = urllib2.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 = urllib2.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 = urllib2.urlopen('http://httpbin.org/').read()
response = urlopen(httpbin.url).read()
assert cass.play_count == 0
with vcr.use_cassette(testfile) as cass:
response2 = urllib2.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 = urllib2.urlopen('http://httpbin.org/').read()
response3 = urlopen(httpbin.url).read()
assert cass.play_count == 1
assert response == response2

View File

@@ -0,0 +1,81 @@
import pytest
boto = pytest.importorskip("boto")
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'))):
# Perform the imports within the patched context so that
# CertValidatingHTTPSConnection refers to the patched version.
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 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
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'))):
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'))):
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'))):
s3_conn = S3Connection()
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'))):
s3_conn = S3Connection()
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')
except DuplicateSectionError:
pass
# Ensure that boto uses HTTPS
boto.config.set('Boto', 'is_secure', 'true')
# Ensure that boto uses CertValidatingHTTPSConnection
boto.config.set('Boto', 'https_validate_certificates', 'true')
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'))):
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

@@ -1,48 +1,50 @@
import os
import json
import urllib2
import pytest
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'
urllib2.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'):
urllib2.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):
urllib2.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(httpbin.url)
with my_vcr.use_cassette(str(tmpdir.join('test.json'))) as cass:
urllib2.urlopen('http://httpbin.org/')
urllib2.urlopen('http://httpbin.org/get')
urlopen(httpbin.url + '/get')
assert len(cass) == 1
assert cass.play_count == 1

View File

@@ -1,28 +1,28 @@
# -*- coding: utf-8 -*-
'''Basic tests about save behavior'''
# coding=utf-8
# External imports
import os
import urllib2
import time
from six.moves.urllib.request import urlopen
# Internal imports
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:
urllib2.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:
urllib2.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:
urllib2.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:
urllib2.urlopen('http://www.iana.org/domains/reserved').read()
urllib2.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

@@ -0,0 +1,132 @@
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):
request = Request(url)
base64string = base64.b64encode(
username.encode('ascii') + b':' + password.encode('ascii')
)
request.add_header(b"Authorization", b"Basic " + base64string)
return urlopen(request)
def _find_header(cassette, header):
return any(header in request.headers for request in cassette.requests)
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
with my_vcr.use_cassette(cass_file, filter_headers=['authorization']):
with pytest.raises(HTTPError):
resp = _request_with_auth(url, 'user', 'wrongpasswd')
assert resp.getcode() == 401
resp = _request_with_auth(url, 'user', 'passwd')
assert resp.getcode() == 200
# make same 2 requests, this time both served from cassette.
with my_vcr.use_cassette(cass_file, filter_headers=['authorization']) as cass:
with pytest.raises(HTTPError):
resp = _request_with_auth(url, 'user', 'wrongpasswd')
assert resp.getcode() == 401
resp = _request_with_auth(url, 'user', 'passwd')
assert resp.getcode() == 200
# authorization header should not have been recorded
assert not _find_header(cass, 'authorization')
assert len(cass) == 2
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)
with vcr.use_cassette(cass_file, filter_query_parameters=['foo']) as cass:
urlopen(url)
assert 'foo' not in cass.requests[0].url
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:
urlopen(url)
assert len(cass) == 0
my_vcr = vcr.VCR(before_record_request=before_record_cb)
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

@@ -0,0 +1,154 @@
# -*- coding: utf-8 -*-
'''Integration tests with httplib2'''
# External imports
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
httplib2 = pytest.importorskip("httplib2")
def http():
"""
Returns an httplib2 HTTP instance
with the certificate replaced by the httpbin one.
"""
return httplib2.Http(ca_certs=pytest_httpbin.certs.where())
def test_response_code(tmpdir, httpbin_both):
'''Ensure we can read a response code from a fetch'''
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'))):
resp, _ = http().request(url)
assert code == resp.status
def test_random_body(httpbin_both, tmpdir):
'''Ensure we can read the content, and that it's served from cache'''
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'))):
_, content = http().request(url)
assert body == content
def test_response_headers(tmpdir, httpbin_both):
'''Ensure we can get information from the response'''
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'))):
resp, _ = http().request(url)
assert set(headers) == set(resp.items())
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 = [
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:
[http().request(url) for url in urls]
assert len(cass) == len(urls)
def test_get_data(tmpdir, httpbin_both):
'''Ensure that it works with query data'''
data = urlencode({'some': 1, 'data': 'here'})
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'))):
_, res2 = http().request(url)
assert res1 == res2
def test_post_data(tmpdir, httpbin_both):
'''Ensure that it works when posting data'''
data = urlencode({'some': 1, 'data': 'here'})
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 = http().request(url, "POST", data)
assert res1 == res2
assert_cassette_has_one_response(cass)
def test_post_unicode_data(tmpdir, httpbin_both):
'''Ensure that it works when posting unicode data'''
data = urlencode({'snowman': u''.encode('utf-8')})
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 = http().request(url, "POST", data)
assert res1 == res2
assert_cassette_has_one_response(cass)
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:
http().request(httpbin_secure.url)
http().request(httpbin.url)
assert len(cass) == 2
assert cass.play_count == 0
def test_decorator(tmpdir, httpbin_both):
'''Test the decorator version of VCR.py'''
url = httpbin_both.url
@vcr.use_cassette(str(tmpdir.join('atts.yaml')))
def inner1():
resp, _ = http().request(url)
return resp['status']
@vcr.use_cassette(str(tmpdir.join('atts.yaml')))
def inner2():
resp, _ = http().request(url)
return resp['status']
assert inner1() == inner2()

View File

@@ -0,0 +1,73 @@
from six.moves.urllib.request import urlopen
import socket
from contextlib import contextmanager
import vcr
@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_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))
assert len(cass) == 0
urlopen('http://httpbin.org:{0}/'.format(httpbin.port))
assert len(cass) == 1
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:{0}/'.format(httpbin.port))
assert len(cass) == 0
urlopen('http://localhost:{0}/'.format(httpbin.port))
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:{0}'.format(httpbin.port))
urlopen('http://localhost:{0}'.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:{0}'.format(httpbin.port))
assert len(cass) == 0
urlopen('http://httpbin.org:{0}'.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))
assert len(cass) == 1

View File

@@ -0,0 +1,113 @@
import vcr
import pytest
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, 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)
return cassette_path
@pytest.mark.parametrize("matcher, matching_uri, not_matching_uri", [
('uri',
'http://httpbin.org/get?p1=q1&p2=q2',
'http://httpbin.org/get?p2=q2&p1=q1'),
('scheme',
'http://google.com/post?a=b',
'https://httpbin.org/get?p1=q1&p2=q2'),
('host',
'https://httpbin.org/post?a=b',
'http://google.com/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(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)
assert cass.play_count == 1
# play cassette with matching on uri
with vcr.use_cassette(cassette, match_on=[matcher]) as cass:
urlopen(matching_uri)
assert cass.play_count == 1
# play cassette with not matching on uri, it should fail
with pytest.raises(vcr.errors.CannotOverwriteExistingCassetteException):
with vcr.use_cassette(cassette, match_on=[matcher]) as cass:
urlopen(not_matching_uri)
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')
assert cass.play_count == 1
# should fail if method does not match
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'')
@pytest.mark.parametrize("uri", [
DEFAULT_URI,
'http://httpbin.org/get?p2=q2&p1=q1',
'http://httpbin.org/get?p2=q2&p1=q1',
])
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
@pytest.mark.parametrize("uri", [
'https://httpbin.org/get?p1=q1&p2=q2',
'http://google.com/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, 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, 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'')

View File

@@ -0,0 +1,20 @@
import pytest
import vcr
from six.moves.urllib.request import urlopen
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(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(httpbin.url + '/status/200').getcode() == 200
assert urlopen(httpbin.url + '/status/201').getcode() == 201
with pytest.raises(Exception):
urlopen(httpbin.url + '/status/200')

View File

@@ -1,59 +1,115 @@
import os
import urllib2
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 = urllib2.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 = urllib2.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 = urllib2.urlopen('http://httpbin.org/get').read()
urlopen(httpbin.url + '/get').read()
def test_new_episodes_record_mode(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
urlopen(httpbin.url).read()
urlopen(httpbin.url).read()
with vcr.use_cassette(testfile, record_mode="once"):
# do it again
urlopen(httpbin.url).read()
urlopen(httpbin.url).read()
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
urlopen(httpbin.url).read()
urlopen(httpbin.url).read()
urlopen(httpbin.url).read()
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 = urllib2.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 = urllib2.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 = urllib2.urlopen('http://httpbin.org/get').read()
urlopen(httpbin.url + '/get').read()
# the first interaction was not re-recorded, but the second was added
# one of the responses has been played
assert cass.play_count == 1
# not all responses have been played
assert not cass.all_played
def test_all_record_mode(tmpdir):
with vcr.use_cassette(testfile, record_mode="new_episodes") as cass:
# the cassette should now have 2 responses
assert len(cass.responses) == 2
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 = urllib2.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 = urllib2.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 = urllib2.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
@@ -61,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 = urllib2.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 = urllib2.urlopen('http://httpbin.org/').read()
urlopen(httpbin.url).read()
# play from cassette file
with vcr.use_cassette(testfile, record_mode="none") as cass:
response = urllib2.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 = urllib2.urlopen('http://httpbin.org/get').read()
urlopen(httpbin.url + '/get').read()

View File

@@ -1,5 +1,5 @@
import urllib2
import vcr
from six.moves.urllib.request import urlopen
def true_matcher(r1, r2):
@@ -10,23 +10,27 @@ def false_matcher(r1, r2):
return False
def test_registered_serializer_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
urllib2.urlopen('http://httpbin.org/')
urllib2.urlopen('https://httpbin.org/get')
assert len(cass) == 1
urlopen(httpbin.url)
urlopen(httpbin.url + '/get')
with my_vcr.use_cassette(testfile, match_on=['true']):
# I can get the response twice even though I only asked for it once
urlopen(httpbin.url + '/get')
urlopen(httpbin.url + '/get')
def test_registered_serializer_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
urllib2.urlopen('http://httpbin.org/')
urllib2.urlopen('https://httpbin.org/get')
urlopen(httpbin.url)
urlopen(httpbin.url + '/get')
assert len(cass) == 2

View File

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

View File

@@ -1,4 +1,3 @@
import urllib2
import vcr
@@ -11,7 +10,7 @@ class MockSerializer(object):
def deserialize(self, cassette_string):
self.serialize_count += 1
self.cassette_string = cassette_string
return ([], [])
return {'interactions': []}
def serialize(self, cassette_dict):
self.deserialize_count += 1
@@ -24,7 +23,6 @@ def test_registered_serializer(tmpdir):
my_vcr.register_serializer('mock', ms)
tmpdir.join('test.mock').write('test_data')
with my_vcr.use_cassette(str(tmpdir.join('test.mock')), serializer='mock'):
urllib2.urlopen('http://httpbin.org/')
# Serializer deserialized once
assert ms.serialize_count == 1
# and serialized the test data string

View File

@@ -1,11 +1,19 @@
import urllib2
import vcr
from six.moves.urllib.request import urlopen
def test_recorded_request_url_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
urllib2.urlopen('http://httpbin.org/redirect/3')
assert cass.requests[0].url == 'http://httpbin.org/redirect/3'
assert cass.requests[3].url == '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, httpbin):
with vcr.use_cassette(str(tmpdir.join('test.yml'))) as cass:
assert len(cass) == 0
urlopen(httpbin.url + '/response-headers?foo=bar&foo=baz')
assert len(cass) == 1
assert cass.responses[0]['headers']['foo'] == ['bar', 'baz']

View File

@@ -1,74 +1,83 @@
# -*- coding: utf-8 -*-
'''Test requests' interaction with vcr'''
# coding=utf-8
import os
import pytest
import vcr
from assertions import assert_cassette_empty, assert_cassette_has_one_response
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/'
with vcr.use_cassette(str(tmpdir.join('atts.yaml'))) as cass:
# Ensure that this is empty to begin with
assert_cassette_empty(cass)
assert requests.get(url).status_code == requests.get(url).status_code
# Ensure that we've now cached a single response
assert_cassette_has_one_response(cass)
url = httpbin_both.url + '/'
with vcr.use_cassette(str(tmpdir.join('atts.yaml'))):
status_code = requests.get(url).status_code
with vcr.use_cassette(str(tmpdir.join('atts.yaml'))):
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/'
with vcr.use_cassette(str(tmpdir.join('headers.yaml'))) as cass:
# Ensure that this is empty to begin with
assert_cassette_empty(cass)
assert requests.get(url).headers == requests.get(url).headers
# Ensure that we've now cached a single response
assert_cassette_has_one_response(cass)
url = httpbin_both + '/'
with vcr.use_cassette(str(tmpdir.join('headers.yaml'))):
headers = requests.get(url).headers
with vcr.use_cassette(str(tmpdir.join('headers.yaml'))):
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'
with vcr.use_cassette(str(tmpdir.join('body.yaml'))) as cass:
# Ensure that this is empty to begin with
assert_cassette_empty(cass)
assert requests.get(url).content == requests.get(url).content
# Ensure that we've now cached a single response
assert_cassette_has_one_response(cass)
url = httpbin_both + '/bytes/1024'
with vcr.use_cassette(str(tmpdir.join('body.yaml'))):
content = requests.get(url).content
with vcr.use_cassette(str(tmpdir.join('body.yaml'))):
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'
with vcr.use_cassette(str(tmpdir.join('auth.yaml'))) as cass:
# Ensure that this is empty to begin with
assert_cassette_empty(cass)
url = httpbin_both + '/basic-auth/user/passwd'
with vcr.use_cassette(str(tmpdir.join('auth.yaml'))):
one = requests.get(url, auth=auth)
with vcr.use_cassette(str(tmpdir.join('auth.yaml'))):
two = requests.get(url, auth=auth)
assert one.content == two.content
assert one.status_code == two.status_code
# Ensure that we've now cached a single response
assert_cassette_has_one_response(cass)
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)
@@ -76,44 +85,197 @@ def test_auth_failed(tmpdir, scheme):
two = requests.get(url, auth=auth)
assert one.content == two.content
assert one.status_code == two.status_code == 401
# Ensure that we've now cached a single response
assert_cassette_has_one_response(cass)
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'
with vcr.use_cassette(str(tmpdir.join('requests.yaml'))) as cass:
# Ensure that this is empty to begin with
assert_cassette_empty(cass)
url = httpbin_both + '/post'
with vcr.use_cassette(str(tmpdir.join('requests.yaml'))):
req1 = requests.post(url, data).content
with vcr.use_cassette(str(tmpdir.join('requests.yaml'))):
req2 = requests.post(url, data).content
assert req1 == req2
# Ensure that we've now cached a single response
assert_cassette_has_one_response(cass)
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.xfail('sys.version_info >= (3, 6)', strict=True, raises=ConnectionError)
def test_post_chunked_binary_secure(tmpdir, httpbin_secure):
'''Ensure that we can send chunked binary without breaking while trying to concatenate bytes with str.'''
data1 = iter([b'data', b'to', b'send'])
data2 = iter([b'data', b'to', b'send'])
url = httpbin_secure.url + '/post'
with vcr.use_cassette(str(tmpdir.join('requests.yaml'))):
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
with vcr.use_cassette(str(tmpdir.join('requests.yaml'))) as cass:
# Ensure that this is empty to begin with
assert_cassette_empty(cass)
assert requests.get(url).content == requests.get(url).content
assert content == requests.get(url).content
# 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, 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, httpbin_both):
'''
Ensure that requests (actually urllib3) is able to automatically decompress
the response body
'''
url = httpbin_both + '/gzip'
response = requests.get(url)
with vcr.use_cassette(str(tmpdir.join('gzip.yaml'))):
response = requests.get(url)
assert_is_json(response.content)
with vcr.use_cassette(str(tmpdir.join('gzip.yaml'))):
assert_is_json(response.content)
def test_session_and_connection_close(tmpdir, httpbin):
'''
This tests the issue in https://github.com/kevin1024/vcrpy/issues/48
If you use a requests.session and the connection is closed, then an
exception is raised in the urllib3 module vendored into requests:
`AttributeError: 'NoneType' object has no attribute 'settimeout'`
'''
with vcr.use_cassette(str(tmpdir.join('session_connection_closed.yaml'))):
session = requests.session()
session.get(httpbin + '/get', headers={'Connection': 'close'})
session.get(httpbin + '/get', headers={'Connection': 'close'})
def test_https_with_cert_validation_disabled(tmpdir, httpbin_secure):
with vcr.use_cassette(str(tmpdir.join('cert_validation_disabled.yaml'))):
requests.get(httpbin_secure.url, verify=False)
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(httpbin + '/get')
with vcr.use_cassette(str(tmpdir.join('test_session_after_unpatched.yaml'))):
session = requests.session()
session.get(httpbin + '/get')
session.get(httpbin + '/status/200')
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()
body = session.get(url).content
# 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(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(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 = httpbin_both + '/bytes/1024'
with vcr.use_cassette(str(tmpdir.join('first_nested.yaml'))):
session = requests.session()
first_body = session.get(url).content
with vcr.use_cassette(str(tmpdir.join('second_nested.yaml'))):
second_body = session.get(url).content
third_body = requests.get(url).content
with vcr.use_cassette(str(tmpdir.join('second_nested.yaml'))):
session = requests.session()
assert session.get(url).content == second_body
with vcr.use_cassette(str(tmpdir.join('first_nested.yaml'))):
assert session.get(url).content == first_body
assert session.get(url).content == third_body
# Make sure that the session can now get content normally.
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:
# 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
# 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

@@ -0,0 +1,85 @@
import vcr
import zlib
import six.moves.http_client as httplib
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(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, httpbin):
testfile = str(tmpdir.join('case_insensitivity.yml'))
# check if headers are case insensitive outside of vcrpy
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(host, port)
# check if headers are case insensitive after vcrpy deserializes headers
inside2 = _headers_are_case_insensitive(host, port)
# behavior should be the same both inside and outside
assert outside == inside == inside2
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, httpbin):
testfile = str(tmpdir.join('multiple_headers.yaml'))
outside = _multiple_header_value(httpbin)
with vcr.use_cassette(testfile):
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())

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,128 +1,146 @@
# -*- coding: utf-8 -*-
'''Integration tests with urllib2'''
# coding=utf-8
# External imports
import os
import urllib2
from urllib import urlencode
import pytest
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):
kwargs['cafile'] = pytest_httpbin.certs.where()
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:
# Ensure that this is empty to begin with
assert_cassette_empty(cass)
assert urllib2.urlopen(url).getcode() == urllib2.urlopen(url).getcode()
# Ensure that we've now cached a single response
assert_cassette_has_one_response(cass)
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'))):
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:
# Ensure that this is empty to begin with
assert_cassette_empty(cass)
assert urllib2.urlopen(url).read() == urllib2.urlopen(url).read()
# Ensure that we've now cached a single response
assert_cassette_has_one_response(cass)
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'))):
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:
# Ensure that this is empty to begin with
assert_cassette_empty(cass)
open1 = urllib2.urlopen(url).info().items()
open2 = urllib2.urlopen(url).info().items()
assert open1 == open2
# Ensure that we've now cached a single response
assert_cassette_has_one_response(cass)
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()
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/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:
for index in range(len(urls)):
url = urls[index]
assert len(cass) == index
assert cass.play_count == index
assert urllib2.urlopen(url).read() == urllib2.urlopen(url).read()
assert len(cass) == index + 1
assert cass.play_count == index + 1
[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:
# Ensure that this is empty to begin with
assert_cassette_empty(cass)
res1 = urllib2.urlopen(url).read()
res2 = urllib2.urlopen(url).read()
assert res1 == res2
# Ensure that we've now cached a single response
assert len(cass) == 1
assert cass.play_count == 1
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'})
url = scheme + '://httpbin.org/post'
data = urlencode({'some': 1, 'data': 'here'}).encode('utf-8')
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:
# Ensure that this is empty to begin with
assert_cassette_empty(cass)
res1 = urllib2.urlopen(url, data).read()
res2 = urllib2.urlopen(url, data).read()
assert res1 == res2
# Ensure that we've now cached a single response
assert_cassette_has_one_response(cass)
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')})
url = scheme + '://httpbin.org/post'
data = urlencode({'snowman': u''.encode('utf-8')}).encode('utf-8')
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:
# Ensure that this is empty to begin with
assert_cassette_empty(cass)
res1 = urllib2.urlopen(url, data).read()
res2 = urllib2.urlopen(url, data).read()
assert res1 == res2
# Ensure that we've now cached a single response
assert_cassette_has_one_response(cass)
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:
urllib2.urlopen('https://httpbin.org/')
urllib2.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(httpbin_both, tmpdir):
'''Test the decorator version of VCR.py'''
url = httpbin_both.url
@vcr.use_cassette(str(tmpdir.join('atts.yaml')))
def inner1():
return urlopen_with_cafile(url).getcode()
@vcr.use_cassette(str(tmpdir.join('atts.yaml')))
def inner2():
return urlopen_with_cafile(url).getcode()
assert inner1() == inner2()

View File

@@ -0,0 +1,140 @@
'''Integration tests with urllib3'''
# coding=utf-8
import pytest
import pytest_httpbin
import vcr
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='{0}:{1}'.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='{0}:{1}'.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)

View File

@@ -1,9 +1,14 @@
import pytest
from six.moves import xmlrpc_client
requests = pytest.importorskip("requests")
import vcr
import vcr # NOQA
import httplib
try:
import httplib
except ImportError:
import http.client as httplib
def test_domain_redirect():
@@ -19,14 +24,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))
@@ -37,11 +42,14 @@ def test_flickr_multipart_upload():
data = r.read()
h.close()
with vcr.use_cassette('fixtures/vcr_cassettes/flickr.json') 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.json') as cass:
with vcr.use_cassette(testfile) as cass:
assert len(cass) == 1
_pretend_to_be_flickr_library()
assert cass.play_count == 1
@@ -52,3 +60,33 @@ def test_flickr_should_respond_with_200(tmpdir):
with vcr.use_cassette(testfile):
r = requests.post("http://api.flickr.com/services/upload")
assert r.status_code == 200
def test_cookies(tmpdir, httpbin):
testfile = str(tmpdir.join('cookies.yml'))
with vcr.use_cassette(testfile):
s = requests.Session()
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
def test_xmlrpclib(tmpdir):
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()
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()
assert original_schema == second_schema

View File

@@ -1,15 +1,25 @@
import copy
import inspect
import os
from six.moves import http_client as httplib
import pytest
import yaml
import mock
from vcr.compat import mock, contextlib
from vcr.cassette import Cassette
from vcr.errors import UnhandledHTTPRequestError
from vcr.patch import force_reset
from vcr.stubs import VCRHTTPSConnection
def test_cassette_load(tmpdir):
a_file = tmpdir.join('test_cassette.yml')
a_file.write(yaml.dump([
{'request': 'foo', 'response': 'bar'}
]))
a_cassette = Cassette.load(str(a_file))
a_file.write(yaml.dump({'interactions': [
{'request': {'body': '', 'uri': 'foo', 'method': 'GET', 'headers': {}},
'response': 'bar'}
]}))
a_cassette = Cassette.load(path=str(a_file))
assert len(a_cassette) == 1
@@ -18,21 +28,6 @@ def test_cassette_not_played():
assert not a.play_count
def test_cassette_played():
a = Cassette('test')
a.mark_played('foo')
a.mark_played('foo')
assert a.play_count == 2
def test_cassette_play_counter():
a = Cassette('test')
a.mark_played('foo')
a.mark_played('bar')
assert a.play_counts['foo'] == 1
assert a.play_counts['bar'] == 1
def test_cassette_append():
a = Cassette('test')
a.append('foo', 'bar')
@@ -59,14 +54,255 @@ def test_cassette_contains():
@mock.patch('vcr.cassette.requests_match', _mock_requests_match)
def test_cassette_response_of():
def test_cassette_responses_of():
a = Cassette('test')
a.append('foo', 'bar')
assert a.response_of('foo') == 'bar'
assert a.responses_of('foo') == ['bar']
@mock.patch('vcr.cassette.requests_match', _mock_requests_match)
def test_cassette_get_missing_response():
a = Cassette('test')
with pytest.raises(KeyError):
a.response_of('foo')
with pytest.raises(UnhandledHTTPRequestError):
a.responses_of('foo')
@mock.patch('vcr.cassette.requests_match', _mock_requests_match)
def test_cassette_cant_read_same_request_twice():
a = Cassette('test')
a.append('foo', 'bar')
a.play_response('foo')
with pytest.raises(UnhandledHTTPRequestError):
a.play_response('foo')
def make_get_request():
conn = httplib.HTTPConnection("www.python.org")
conn.request("GET", "/index.html")
return conn.getresponse()
@mock.patch('vcr.cassette.requests_match', return_value=True)
@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(path='test')(make_get_request)
for i in range(4):
decorated_function()
def test_arg_getter_functionality():
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 = {'path': 'other'}
with context_decorator as cassette:
assert cassette._path == 'other'
arg_getter.return_value = {'path': 'other', 'filter_headers': ('header_name',)}
@context_decorator
def function():
pass
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)
def test_cassette_not_all_played():
a = Cassette('test')
a.append('foo', 'bar')
assert not a.all_played
@mock.patch('vcr.cassette.requests_match', _mock_requests_match)
def test_cassette_all_played():
a = Cassette('test')
a.append('foo', 'bar')
a.play_response('foo')
assert a.all_played
def test_before_record_response():
before_record_response = mock.Mock(return_value='mutated')
cassette = Cassette('test', before_record_response=before_record_response)
cassette.append('req', 'res')
before_record_response.assert_called_once_with('res')
assert cassette.responses[0] == 'mutated'
def assert_get_response_body_is(value):
conn = httplib.HTTPConnection("www.python.org")
conn.request("GET", "/index.html")
assert conn.getresponse().read().decode('utf8') == value
@mock.patch('vcr.cassette.requests_match', _mock_requests_match)
@mock.patch('vcr.cassette.Cassette.can_play_response_for', return_value=True)
@mock.patch('vcr.cassette.Cassette._save', return_value=True)
def test_nesting_cassette_context_managers(*args):
first_response = {'body': {'string': b'first_response'}, 'headers': {},
'status': {'message': 'm', 'code': 200}}
second_response = copy.deepcopy(first_response)
second_response['body']['string'] = b'second_response'
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))
assert_get_response_body_is('first_response')
# Make sure a second cassette can supercede the first
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')
# Now the first cassette should be back in effect
assert_get_response_body_is('first_response')
def test_nesting_context_managers_by_checking_references_of_http_connection():
original = httplib.HTTPConnection
with Cassette.use(path='test'):
first_cassette_HTTPConnection = httplib.HTTPConnection
with Cassette.use(path='test'):
second_cassette_HTTPConnection = httplib.HTTPConnection
assert second_cassette_HTTPConnection is not first_cassette_HTTPConnection
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]

274
tests/unit/test_filters.py Normal file
View File

@@ -0,0 +1,274 @@
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_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_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_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

@@ -0,0 +1,19 @@
import pytest
from vcr.serializers.jsonserializer import serialize
from vcr.request import Request
def test_serialize_binary():
request = Request(
method='GET',
uri='http://localhost/',
body='',
headers={},
)
cassette = {'requests': [request], 'responses': [{'body': b'\x8c'}]}
with pytest.raises(Exception) as e:
serialize(cassette)
assert e.message == "Error serializing cassette to JSON. Does this \
HTTP interaction contain binary data? If so, use a different \
serializer (like the yaml serializer) for this request"

159
tests/unit/test_matchers.py Normal file
View File

@@ -0,0 +1,159 @@
import itertools
import pytest
from vcr import matchers
from vcr import request
# the dict contains requests with corresponding to its key difference
# with 'base' request.
REQUESTS = {
'base': request.Request('GET', 'http://host.com/p?a=b', '', {}),
'method': request.Request('POST', 'http://host.com/p?a=b', '', {}),
'scheme': request.Request('GET', 'https://host.com:80/p?a=b', '', {}),
'host': request.Request('GET', 'http://another-host.com/p?a=b', '', {}),
'port': request.Request('GET', 'http://host.com:90/p?a=b', '', {}),
'path': request.Request('GET', 'http://host.com/x?a=b', '', {}),
'query': request.Request('GET', 'http://host.com/p?c=d', '', {}),
}
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)):
assert not matched
else:
assert matched
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')):
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', '', {})
assert matchers.query(req1, req2)
req1 = request.Request('GET', 'http://host.com/?a=b&a=b&c=d', '', {})
req2 = request.Request('GET', 'http://host.com/?a=b&c=d&a=b', '', {})
req3 = request.Request('GET', 'http://host.com/?c=d&a=b&a=b', '', {})
assert matchers.query(req1, req2)
assert matchers.query(req1, req3)
def test_metchers():
assert_matcher('method')
assert_matcher('scheme')
assert_matcher('host')
assert_matcher('port')
assert_matcher('path')
assert_matcher('query')

View File

@@ -0,0 +1,41 @@
import filecmp
import json
import shutil
import yaml
import vcr.migration
def test_try_migrate_with_json(tmpdir):
cassette = tmpdir.join('cassette.json').strpath
shutil.copy('tests/fixtures/migration/old_cassette.json', cassette)
assert vcr.migration.try_migrate(cassette)
with open('tests/fixtures/migration/new_cassette.json', 'r') as f:
expected_json = json.load(f)
with open(cassette, 'r') as f:
actual_json = json.load(f)
assert actual_json == expected_json
def test_try_migrate_with_yaml(tmpdir):
cassette = tmpdir.join('cassette.yaml').strpath
shutil.copy('tests/fixtures/migration/old_cassette.yaml', cassette)
assert vcr.migration.try_migrate(cassette)
with open('tests/fixtures/migration/new_cassette.yaml', 'r') as f:
expected_yaml = yaml.load(f)
with open(cassette, 'r') as f:
actual_yaml = yaml.load(f)
assert actual_yaml == expected_yaml
def test_try_migrate_with_invalid_or_new_cassettes(tmpdir):
cassette = tmpdir.join('cassette').strpath
files = [
'tests/fixtures/migration/not_cassette.txt',
'tests/fixtures/migration/new_cassette.yaml',
'tests/fixtures/migration/new_cassette.json',
]
for file_path in files:
shutil.copy(file_path, cassette)
assert not vcr.migration.try_migrate(cassette)
assert filecmp.cmp(cassette, file_path) # shold not change file

View File

@@ -0,0 +1,24 @@
import pytest
from vcr.persisters.filesystem import FilesystemPersister
from vcr.serializers import jsonserializer, yamlserializer
@pytest.mark.parametrize("cassette_path, serializer", [
('tests/fixtures/migration/old_cassette.json', jsonserializer),
('tests/fixtures/migration/old_cassette.yaml', yamlserializer),
])
def test_load_cassette_with_old_cassettes(cassette_path, serializer):
with pytest.raises(ValueError) as excinfo:
FilesystemPersister.load_cassette(cassette_path, serializer)
assert "run the migration script" in excinfo.exconly()
@pytest.mark.parametrize("cassette_path, serializer", [
('tests/fixtures/migration/not_cassette.txt', jsonserializer),
('tests/fixtures/migration/not_cassette.txt', yamlserializer),
])
def test_load_cassette_with_invalid_cassettes(cassette_path, serializer):
with pytest.raises(Exception) as excinfo:
FilesystemPersister.load_cassette(cassette_path, serializer)
assert "run the migration script" not in excinfo.exconly()

View File

@@ -1,11 +1,70 @@
from vcr.request import Request
import pytest
def test_url():
req = Request('http', 'www.google.com', 80, 'GET', '/', '', {})
assert req.url == 'http://www.google.com/'
from vcr.request import Request, HeadersDict
def test_str():
req = Request('http', 'www.google.com', 80, 'GET', '/', '', {})
str(req) == '<Request (GET) http://www.google.com>'
req = Request('GET', 'http://www.google.com/', '', {})
str(req) == '<Request (GET) http://www.google.com/>'
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.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/', 443),
('https://go.com:443/', 443),
('https://go.com:3000/', 3000),
])
def test_port(uri, expected_port):
req = Request('GET', uri, '', {})
assert req.port == expected_port
def test_uri():
req = Request('GET', 'http://go.com/', '', {})
assert req.uri == 'http://go.com/'
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

@@ -0,0 +1,139 @@
# -*- encoding: utf-8 -*-
import pytest
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():
with open('tests/fixtures/migration/old_cassette.yaml', 'r') as f:
with pytest.raises(ValueError):
deserialize(f.read(), yamlserializer)
def test_deserialize_old_json_cassette():
with open('tests/fixtures/migration/old_cassette.json', 'r') as f:
with pytest.raises(ValueError):
deserialize(f.read(), jsonserializer)
def test_deserialize_new_yaml_cassette():
with open('tests/fixtures/migration/new_cassette.yaml', 'r') as f:
deserialize(f.read(), yamlserializer)
def test_deserialize_new_json_cassette():
with open('tests/fixtures/migration/new_cassette.json', 'r') as f:
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

366
tests/unit/test_vcr.py Normal file
View File

@@ -0,0 +1,366 @@
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',
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
# Make sure that calls to function now use cassettes with the
# new filter_header_settings
test_vcr.record_mode = mock.Mock()
function()
assert mock_cassette_load.call_args[1]['record_mode'] == test_vcr.record_mode
# Ensure that explicitly provided arguments still supercede
# those on the vcr.
new_record_mode = mock.Mock()
with test_vcr.use_cassette('test', record_mode=new_record_mode) as cassette:
assert cassette.record_mode == new_record_mode
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', ('bert', 'ernie')),
before_record_request=before_record_cb,
ignore_hosts=('www.test.com',), ignore_localhost=True,
filter_query_parameters=('foo', ('tom', 'jerry')),
filter_post_data_parameters=('posted', ('no', 'trespassing')))
with test_vcr.use_cassette('test') as cassette:
# 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
# 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')]
# 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 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
def random_fixture():
return 1
@use_cassette('test')
def test_fixtures_with_use_cassette(random_fixture):
# Applying a decorator to a test function that requests features can cause
# 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.
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) == set([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']])
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

65
tox.ini
View File

@@ -1,35 +1,46 @@
# Tox (http://tox.testrun.org/) is a tool for running tests
# in multiple virtualenvs. This configuration file will run the
# test suite on all supported python versions. To use it, "pip install tox"
# and then run "tox" from this directory.
[tox]
envlist = py26, py27, pypy, py26requests, py27requests, pypyrequests
envlist = {py26,py27,py33,py34,py35,py36,pypy,pypy3}-{flakes,requests213,requests27,requests26,requests25,requests24,requests23,requests22,requests1,httplib2,urllib317,urllib319,urllib3110,tornado3,tornado4,boto,boto3,aiohttp}
[testenv:flakes]
skipsdist = True
commands =
flake8 --version
flake8 --exclude=./docs/conf.py,./.tox/
pyflakes ./docs/conf.py
deps = flake8
[testenv]
commands =
python setup.py test
./runtests.sh {posargs}
deps =
# httpbin fails with latest Flask, so we pin it
Flask==0.10.1
mock
pytest
pytest-httpbin
PyYAML
requests1: requests==1.2.3
requests213: requests==2.13.0
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,py35,py36,pypy}-tornado3: tornado>=3,<4
{py26,py27,py33,py34,py35,py36,pypy}-tornado4: tornado>=4,<5
{py26,py27,py33,py34,py35,py36,pypy}-tornado3: pytest-tornado
{py26,py27,py33,py34,py35,py36,pypy}-tornado4: pytest-tornado
{py26,py27,py33,py34,py35,py36}-tornado3: pycurl
{py26,py27,py33,py34,py35,py36}-tornado4: pycurl
boto: boto
boto3: boto3
aiohttp: aiohttp
aiohttp: pytest-asyncio
[testenv:py26requests]
basepython = python2.6
deps =
pytest
PyYAML
requests
[testenv:py27requests]
basepython = python2.7
deps =
pytest
PyYAML
requests
[testenv:pypyrequests]
basepython = pypy
deps =
pytest
PyYAML
requests
[flake8]
max_line_length = 110

View File

@@ -1,8 +1,17 @@
from config import VCR
import logging
from .config import VCR
# Set default logging handler to avoid "No handler found" warnings.
try: # Python 2.7+
from logging import NullHandler
except ImportError:
class NullHandler(logging.Handler):
def emit(self, record):
pass
logging.getLogger(__name__).addHandler(NullHandler())
default_vcr = VCR()
# Also, make a 'load' function available
def use_cassette(path, **kwargs):
return default_vcr.use_cassette(path, **kwargs)
use_cassette = default_vcr.use_cassette

7
vcr/_handle_coroutine.py Normal file
View File

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

View File

@@ -1,47 +1,208 @@
'''The container for recorded requests and responses'''
import sys
import inspect
import logging
import wrapt
from .compat import contextlib, collections
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 collections import Counter, OrderedDict
from asyncio import iscoroutinefunction
from ._handle_coroutine import handle_coroutine
except ImportError:
from .compat.counter import Counter
from .compat.ordereddict import OrderedDict
def iscoroutinefunction(*args, **kwargs):
return False
# Internal imports
from .patch import install, reset
from .persist import load_cassette, save_cassette
from .serializers import yamlserializer
from .matchers import requests_match, url, method
def handle_coroutine(*args, **kwags):
raise NotImplementedError('Not implemented on Python 2')
log = logging.getLogger(__name__)
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.
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, **kwargs):
return cls(cassette_class, lambda: dict(kwargs))
def __init__(self, cls, args_getter):
self.cls = cls
self._args_getter = args_getter
self.__finish = None
def _patch_generator(self, cassette):
with contextlib.ExitStack() as exit_stack:
for patcher in CassettePatcherBuilder(cassette).build():
exit_stack.enter_context(patcher)
log_format = '{action} context for cassette at {path}.'
log.debug(log_format.format(
action="Entering", path=cassette._path
))
yield cassette
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."
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):
next(self.__finish, None)
self.__finish = None
@wrapt.decorator
def __call__(self, function, instance, 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:
to_yield = coroutine.send(to_send)
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'''
"""A container for recorded requests and responses"""
@classmethod
def load(cls, path, **kwargs):
'''Load in the cassette stored at the provided path'''
new_cassette = cls(path, **kwargs)
def load(cls, **kwargs):
"""Instantiate and load the cassette stored at the specified path."""
new_cassette = cls(**kwargs)
new_cassette._load()
return new_cassette
def __init__(self,
path,
serializer=yamlserializer,
record_mode='once',
match_on=[url, method]):
@classmethod
def use_arg_getter(cls, arg_getter):
return CassetteContextDecorator(cls, arg_getter)
@classmethod
def use(cls, **kwargs):
return CassetteContextDecorator.from_args(cls, **kwargs)
def __init__(self, path, serializer=yamlserializer, persister=FilesystemPersister, record_mode='once',
match_on=(uri, method), before_record_request=None,
before_record_response=None, custom_patches=(),
inject=False):
self._persister = persister
self._path = path
self._serializer = serializer
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.record_mode = record_mode
self.rewound = False
@property
def play_count(self):
return sum(self.play_counts.values())
@property
def all_played(self):
"""Returns True if all responses have been played, False otherwise."""
return self.play_count == len(self)
@property
def requests(self):
return [request for (request, response) in self.data]
@@ -50,70 +211,95 @@ class Cassette(object):
def responses(self):
return [response for (request, response) in self.data]
@property
def rewound(self):
"""
If the cassette has already been recorded in another session, and has
been loaded again fresh from disk, it has been "rewound". This means
that it should be write-only, depending on the record mode specified
"""
return not self.dirty and self.play_count
@property
def write_protected(self):
return self.rewound and self.record_mode == 'once' or \
self.record_mode == 'none'
def mark_played(self, request):
'''
Alert the cassette of a request that's been played
'''
self.play_counts[request] += 1
def append(self, request, response):
'''Add a request, response pair to this cassette'''
"""Add a request, response pair to this cassette"""
request = self._before_record_request(request)
if not request:
return
response = self._before_record_response(response)
if response is None:
return
self.data.append((request, response))
self.dirty = True
def response_of(self, request):
'''
Find the response corresponding to a request
def filter_request(self, request):
return self._before_record_request(request)
'''
responses = []
for stored_request, response in self.data:
def _responses(self, request):
"""
internal API, returns an iterator with all responses matching
the request.
"""
request = self._before_record_request(request)
for index, (stored_request, response) in enumerate(self.data):
if requests_match(request, stored_request, self._match_on):
responses.append(response)
index = self.play_counts[request]
try:
return responses[index]
except IndexError:
# I decided that a KeyError is the best exception to raise
# if the cassette doesn't contain the request asked for.
raise KeyError
yield index, response
def can_play_response_for(self, request):
request = self._before_record_request(request)
return request and request in self and \
self.record_mode != 'all' and \
self.rewound
def play_response(self, request):
"""
Get the response corresponding to a request, but only if it
hasn't been played back before, and mark it as played
"""
for index, response in self._responses(request):
if self.play_counts[index] == 0:
self.play_counts[index] += 1
return response
# The cassette doesn't contain the request asked for.
raise UnhandledHTTPRequestError(
"The cassette (%r) doesn't contain the request (%r) asked for"
% (self._path, request)
)
def responses_of(self, request):
"""
Find the responses corresponding to a request.
This function isn't actually used by VCR internally, but is
provided as an external API.
"""
responses = [response for index, response in self._responses(request)]
if responses:
return responses
# The cassette doesn't contain the request asked for.
raise UnhandledHTTPRequestError(
"The cassette (%r) doesn't contain the request (%r) asked for"
% (self._path, request)
)
def _as_dict(self):
return {"requests": self.requests, "responses": self.responses}
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
except IOError:
self.rewound = True
except ValueError:
pass
def __str__(self):
@@ -122,21 +308,12 @@ class Cassette(object):
)
def __len__(self):
'''Return the number of request,response pairs stored in here'''
"""Return the number of request,response pairs stored in here"""
return len(self.data)
def __contains__(self, request):
'''Return whether or not a request has been stored'''
for stored_request, response in self.data:
if requests_match(stored_request, request, self._match_on):
"""Return whether or not a request has been stored"""
for index, response in self._responses(request):
if self.play_counts[index] == 0:
return True
return False
def __enter__(self):
'''Patch the fetching libraries we know about'''
install(self)
return self
def __exit__(self, typ, value, traceback):
self._save()
reset()

18
vcr/compat.py Normal file
View File

@@ -0,0 +1,18 @@
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
import collections
if not hasattr(collections, 'Counter'):
import backport_collections as collections
__all__ = ['mock', 'contextlib', 'collections']

View File

View File

@@ -1,193 +0,0 @@
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,16 +1,43 @@
import copy
import functools
import inspect
import os
import types
import six
from .compat import collections
from .cassette import Cassette
from .serializers import yamlserializer, jsonserializer
from .matchers import method, url, host, path, headers, body
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",
match_on=['url', 'method'],
):
@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
@@ -19,55 +46,236 @@ class VCR(object):
'json': jsonserializer,
}
self.matchers = {
'method': method,
'url': url,
'host': host,
'path': path,
'headers': headers,
'body': body,
'method': matchers.method,
'uri': matchers.uri,
'url': matchers.uri, # matcher for backwards compatibility
'scheme': matchers.scheme,
'host': matchers.host,
'port': matchers.port,
'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(
"Serializer {0} doesn't exist or isn't registered".format(
serializer_name
)
)
raise KeyError
return serializer
def _get_matchers(self, matcher_names):
matchers = []
try:
matchers = [self.matchers[m] for m in matcher_names]
for m in matcher_names:
matchers.append(self.matchers[m])
except KeyError:
raise KeyError(
"Matcher {0} doesn't exist or isn't registered".format(m)
)
return matchers
def use_cassette(self, path, **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:
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_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),
"record_mode": kwargs.get('record_mode', self.record_mode),
'serializer': self._get_serializer(serializer_name),
'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),
'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
}
path = kwargs.get('path')
if path:
merged_config['path'] = path
return merged_config
return Cassette.load(path, **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 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:
break
response = function(response)
return response
return before_record_response
def _build_before_record_request(self, options):
filter_functions = []
filter_headers = options.get(
'filter_headers', self.filter_headers
)
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)
)
ignore_hosts = options.get(
'ignore_hosts', self.ignore_hosts
)
ignore_localhost = options.get(
'ignore_localhost', self.ignore_localhost
)
if 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:
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 = set(ignore_hosts)
if ignore_localhost:
hosts_to_ignore.update(('localhost', '0.0.0.0', '127.0.0.1'))
if 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,)
filter_functions.extend(before_record_request)
def before_record_request(request):
request = copy.copy(request)
for function in filter_functions:
if request is None:
break
request = function(request)
return request
return before_record_request
@staticmethod
def _build_ignore_hosts(hosts_to_ignore):
def filter_ignored_hosts(request):
if hasattr(request, 'host') and request.host in hosts_to_ignore:
return
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))

7
vcr/errors.py Normal file
View File

@@ -0,0 +1,7 @@
class CannotOverwriteExistingCassetteException(Exception):
pass
class UnhandledHTTPRequestError(KeyError):
"""Raised when a cassette does not contain the request we want."""
pass

163
vcr/filters.py Normal file
View File

@@ -0,0 +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):
"""
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):
"""
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,26 +1,101 @@
import json
from six.moves import urllib, xmlrpc_client
from .util import read_body
import logging
log = logging.getLogger(__name__)
def method(r1, r2):
return r1.method == r2.method
def url(r1, r2):
return r1.url == r2.url
def uri(r1, r2):
return r1.uri == r2.uri
def host(r1, r2):
return r1.host == r2.host
def scheme(r1, r2):
return r1.scheme == r2.scheme
def port(r1, r2):
return r1.port == r2.port
def path(r1, r2):
return r1.path == r2.path
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):
return r1.headers == r2.headers
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)
)
def requests_match(r1, r2, matchers):
return all(m(r1, r2) for m in matchers)
matches = [(m(r1, r2), m) for m in matchers]
_log_matches(r1, r2, matches)
return all([m[0] for m in matches])

169
vcr/migration.py Normal file
View File

@@ -0,0 +1,169 @@
"""
Migration script for old 'yaml' and 'json' cassettes
.. warning:: Backup your cassettes files before migration.
It merges and deletes the request obsolete keys (protocol, host, port, path)
into new 'uri' key.
Usage::
python -m vcr.migration PATH
The PATH can be path to the directory with cassettes or cassette itself
"""
import json
import os
import shutil
import sys
import tempfile
import yaml
from .serializers import yamlserializer, jsonserializer
from .serialize import serialize
from . import request
from .stubs.compat import get_httpmessage
# Use the libYAML versions if possible
try:
from yaml import CLoader as Loader
except ImportError:
from yaml import Loader
def preprocess_yaml(cassette):
# this is the hack that makes the whole thing work. The old version used
# to deserialize to Request objects automatically using pyYaml's !!python
# tag system. This made it difficult to deserialize old cassettes on new
# versions. So this just strips the tags before deserializing.
STRINGS_TO_NUKE = [
'!!python/object:vcr.request.Request',
'!!python/object/apply:__builtin__.frozenset',
'!!python/object/apply:builtins.frozenset',
]
for s in STRINGS_TO_NUKE:
cassette = cassette.replace(s, '')
return cassette
PARTS = [
'protocol',
'host',
'port',
'path',
]
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 ''
return "{protocol}://{host}{port}{path}".format(**parts)
def _migrate(data):
interactions = []
for item in data:
req = item['request']
res = item['response']
uri = dict((k, req.pop(k)) for k in PARTS)
req['uri'] = build_uri(**uri)
# convert headers to dict of lists
headers = req['headers']
for k in headers:
headers[k] = [headers[k]]
response_headers = {}
for k, v in get_httpmessage(
b"".join(h.encode('utf-8') for h in res['headers'])
).items():
response_headers.setdefault(k, [])
response_headers[k].append(v)
res['headers'] = response_headers
interactions.append({'request': req, 'response': res})
return {
'requests': [
request.Request._from_dict(i['request']) for i in interactions
],
'responses': [i['response'] for i in interactions],
}
def migrate_json(in_fp, out_fp):
data = json.load(in_fp)
if _already_migrated(data):
return False
interactions = _migrate(data)
out_fp.write(serialize(interactions, jsonserializer))
return True
def _list_of_tuples_to_dict(fs):
return dict((k, v) for k, v in fs[0])
def _already_migrated(data):
try:
if data.get('version') == 1:
return True
except AttributeError:
return False
def migrate_yml(in_fp, out_fp):
data = yaml.load(preprocess_yaml(in_fp.read()), Loader=Loader)
if _already_migrated(data):
return False
for i in range(len(data)):
data[i]['request']['headers'] = _list_of_tuples_to_dict(
data[i]['request']['headers']
)
interactions = _migrate(data)
out_fp.write(serialize(interactions, yamlserializer))
return True
def migrate(file_path, migration_fn):
# because we assume that original files can be reverted
# we will try to copy the content. (os.rename not needed)
with tempfile.TemporaryFile(mode='w+') as out_fp:
with open(file_path, 'r') as in_fp:
if not migration_fn(in_fp, out_fp):
return False
with open(file_path, 'w') as in_fp:
out_fp.seek(0)
shutil.copyfileobj(out_fp, in_fp)
return True
def try_migrate(path):
if path.endswith('.json'):
return migrate(path, migrate_json)
elif path.endswith('.yaml') or path.endswith('.yml'):
return migrate(path, migrate_yml)
return False
def main():
if len(sys.argv) != 2:
raise SystemExit("Please provide path to cassettes directory or file. "
"Usage: python -m vcr.migration PATH")
path = sys.argv[1]
if not os.path.isabs(path):
path = os.path.abspath(path)
files = [path]
if os.path.isdir(path):
files = (os.path.join(root, name)
for (root, dirs, files) in os.walk(path)
for name in files)
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("Done.\n")
if __name__ == '__main__':
main()

View File

@@ -1,79 +1,455 @@
'''Utilities for patching in cassettes'''
import functools
import itertools
import httplib
from .compat import contextlib, mock
from .stubs import VCRHTTPConnection, VCRHTTPSConnection
from six.moves import http_client as httplib
# Save some of the original types for the purposes of unpatching
_HTTPConnection = httplib.HTTPConnection
_HTTPSConnection = httplib.HTTPSConnection
# Try to save the original types for requests
try:
# Try to save the original types for requests
import requests.packages.urllib3.connectionpool as cpool
_VerifiedHTTPSConnection = cpool.VerifiedHTTPSConnection
_HTTPConnection = cpool.HTTPConnection
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
except ImportError: # pragma: no cover
pass
else:
_Boto3VerifiedHTTPSConnection = cpool.VerifiedHTTPSConnection
_cpoolBoto3HTTPConnection = cpool.HTTPConnection
_cpoolBoto3HTTPSConnection = cpool.HTTPSConnection
# Try to save the original types for urllib3
try:
import urllib3
except ImportError: # pragma: no cover
pass
else:
_VerifiedHTTPSConnection = urllib3.connectionpool.VerifiedHTTPSConnection
# Try to save the original types for httplib2
try:
import httplib2
except ImportError: # pragma: no cover
pass
else:
_HTTPConnectionWithTimeout = httplib2.HTTPConnectionWithTimeout
_HTTPSConnectionWithTimeout = httplib2.HTTPSConnectionWithTimeout
_SCHEME_TO_CONNECTION = httplib2.SCHEME_TO_CONNECTION
# Try to save the original types for boto
try:
import boto.https_connection
except ImportError: # pragma: no cover
pass
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:
# Try to save the original types for urllib3
import urllib3
_VerifiedHTTPSConnection = urllib3.connectionpool.VerifiedHTTPSConnection
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
def install(cassette):
"""
Patch all the HTTPConnections references we can find!
This replaces the actual HTTPConnection with a VCRHTTPConnection
object which knows how to save to / read from cassettes
"""
httplib.HTTPConnection = httplib.HTTP._connection_class = VCRHTTPConnection
httplib.HTTPSConnection = httplib.HTTPS._connection_class = (
VCRHTTPSConnection)
httplib.HTTPConnection.cassette = cassette
httplib.HTTPSConnection.cassette = cassette
class CassettePatcherBuilder(object):
# patch requests
def _build_patchers_from_mock_triples_decorator(function):
@functools.wraps(function)
def wrapped(self, *args, **kwargs):
return self._build_patchers_from_mock_triples(
function(self, *args, **kwargs)
)
return wrapped
def __init__(self, cassette):
self._cassette = cassette
self._class_to_cassette_subclass = {}
def build(self):
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:
patcher = self._build_patcher(*args)
if patcher:
yield patcher
def _build_patcher(self, obj, patched_attribute, replacement_class):
if not hasattr(obj, patched_attribute):
return
return mock.patch.object(obj, patched_attribute,
self._recursively_apply_get_cassette_subclass(
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():
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)
return replacement_dict_or_obj
def _get_cassette_subclass(self, klass):
if klass.cassette is not None:
return klass
if klass not in self._class_to_cassette_subclass:
subclass = self._build_cassette_subclass(klass)
self._class_to_cassette_subclass[klass] = subclass
return self._class_to_cassette_subclass[klass]
def _build_cassette_subclass(self, base_class):
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),
bases, dict(cassette=self._cassette))
@_build_patchers_from_mock_triples_decorator
def _httplib(self):
yield httplib, 'HTTPConnection', VCRHTTPConnection
yield httplib, 'HTTPSConnection', VCRHTTPSConnection
def _requests(self):
try:
import requests.packages.urllib3.connectionpool as cpool
except ImportError: # pragma: no cover
return ()
from .stubs import requests_stubs
return self._urllib3_patchers(cpool, requests_stubs)
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())
# 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
def _urllib3(self):
try:
import urllib3.connectionpool as cpool
except ImportError: # pragma: no cover
return ()
from .stubs import urllib3_stubs
return self._urllib3_patchers(cpool, urllib3_stubs)
@_build_patchers_from_mock_triples_decorator
def _httplib2(self):
try:
import httplib2 as cpool
except ImportError: # pragma: no cover
pass
else:
from .stubs.httplib2_stubs import VCRHTTPConnectionWithTimeout
from .stubs.httplib2_stubs import VCRHTTPSConnectionWithTimeout
yield cpool, 'HTTPConnectionWithTimeout', VCRHTTPConnectionWithTimeout
yield cpool, 'HTTPSConnectionWithTimeout', VCRHTTPSConnectionWithTimeout
yield cpool, 'SCHEME_TO_CONNECTION', {'http': VCRHTTPConnectionWithTimeout,
'https': VCRHTTPSConnectionWithTimeout}
@_build_patchers_from_mock_triples_decorator
def _boto(self):
try:
import boto.https_connection as cpool
except ImportError: # pragma: no cover
pass
else:
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):
def __init__(self, connection_class):
self._connection_class = connection_class
self._connection_pool_to_connections = {}
def add_connection_to_pool_entry(self, pool, connection):
if isinstance(connection, self._connection_class):
self._connection_pool_to_connections.setdefault(pool, set()).add(connection)
def remove_connection_to_pool_entry(self, pool, connection):
if isinstance(connection, self._connection_class):
self._connection_pool_to_connections[self._connection_class].remove(connection)
def __enter__(self):
return self
def __exit__(self, *args):
for pool, connections in self._connection_pool_to_connections.items():
readd_connections = []
while pool.pool and not pool.pool.empty() and connections:
connection = pool.pool.get()
if isinstance(connection, self._connection_class):
connections.remove(connection)
else:
readd_connections.append(connection)
for connection in readd_connections:
pool._put_conn(connection)
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
from .stubs.requests_stubs import VCRVerifiedHTTPSConnection
cpool.VerifiedHTTPSConnection = VCRVerifiedHTTPSConnection
cpool.VerifiedHTTPSConnection.cassette = cassette
cpool.HTTPConnection = VCRHTTPConnection
cpool.HTTPConnection.cassette = cassette
except ImportError: # pragma: no cover
pass
else:
# unpatch requests v1.x
yield mock.patch.object(cpool, 'VerifiedHTTPSConnection', _VerifiedHTTPSConnection)
yield mock.patch.object(cpool, 'HTTPConnection', _cpoolHTTPConnection)
# unpatch requests v2.x
if hasattr(cpool.HTTPConnectionPool, 'ConnectionCls'):
yield mock.patch.object(cpool.HTTPConnectionPool, 'ConnectionCls',
_cpoolHTTPConnection)
yield mock.patch.object(cpool.HTTPSConnectionPool, 'ConnectionCls',
_cpoolHTTPSConnection)
# patch urllib3
try:
import urllib3.connectionpool as cpool
from .stubs.urllib3_stubs import VCRVerifiedHTTPSConnection
cpool.VerifiedHTTPSConnection = VCRVerifiedHTTPSConnection
cpool.VerifiedHTTPSConnection.cassette = cassette
cpool.HTTPConnection = VCRHTTPConnection
cpool.HTTPConnection.cassette = cassette
except ImportError: # pragma: no cover
pass
def reset():
'''Undo all the patching'''
httplib.HTTPConnection = httplib.HTTP._connection_class = _HTTPConnection
httplib.HTTPSConnection = httplib.HTTPS._connection_class = \
_HTTPSConnection
try:
import requests.packages.urllib3.connectionpool as cpool
cpool.VerifiedHTTPSConnection = _VerifiedHTTPSConnection
cpool.HTTPConnection = _HTTPConnection
except ImportError: # pragma: no cover
pass
if hasattr(cpool, 'HTTPSConnection'):
yield mock.patch.object(cpool, 'HTTPSConnection', _cpoolHTTPSConnection)
try:
import urllib3.connectionpool as cpool
cpool.VerifiedHTTPSConnection = _VerifiedHTTPSConnection
cpool.HTTPConnection = _HTTPConnection
except ImportError: # pragma: no cover
pass
else:
yield mock.patch.object(cpool, 'VerifiedHTTPSConnection', _VerifiedHTTPSConnection)
yield mock.patch.object(cpool, 'HTTPConnection', _HTTPConnection)
yield mock.patch.object(cpool, 'HTTPSConnection', _HTTPSConnection)
if hasattr(cpool.HTTPConnectionPool, 'ConnectionCls'):
yield mock.patch.object(cpool.HTTPConnectionPool, 'ConnectionCls', _HTTPConnection)
yield mock.patch.object(cpool.HTTPSConnectionPool, 'ConnectionCls', _HTTPSConnection)
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
except ImportError: # pragma: no cover
pass
else:
yield mock.patch.object(cpool, 'HTTPConnectionWithTimeout', _HTTPConnectionWithTimeout)
yield mock.patch.object(cpool, 'HTTPSConnectionWithTimeout', _HTTPSConnectionWithTimeout)
yield mock.patch.object(cpool, 'SCHEME_TO_CONNECTION', _SCHEME_TO_CONNECTION)
try:
import boto.https_connection as cpool
except ImportError: # pragma: no cover
pass
else:
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,
)
@contextlib.contextmanager
def force_reset():
with contextlib.ExitStack() as exit_stack:
for patcher in reset_patchers():
exit_stack.enter_context(patcher)
yield

View File

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

View File

@@ -1,23 +1,26 @@
import tempfile
# .. _persister_example:
import os
from ..serialize import serialize, deserialize
class FilesystemPersister(object):
@classmethod
def _secure_write(cls, path, contents):
"""
We'll overwrite the old version securely by writing out a temporary
version and then moving it to replace the old version
"""
dirname, filename = os.path.split(path)
fd, name = tempfile.mkstemp(dir=dirname, prefix=filename)
with os.fdopen(fd, 'w') as fout:
fout.write(contents)
os.rename(name, path)
@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)
cls._secure_write(cassette_path, data)
with open(cassette_path, 'w') as f:
f.write(data)

View File

@@ -1,52 +1,131 @@
class Request(object):
import warnings
from six import BytesIO, text_type
from six.moves.urllib.parse import urlparse, parse_qsl
from .util import CaseInsensitiveDict
def __init__(self, protocol, host, port, method, path, body, headers):
self.protocol = protocol
self.host = host
self.port = port
class Request(object):
"""
VCR's representation of a request.
"""
def __init__(self, method, uri, body, headers):
self.method = method
self.path = path
self.body = body
# make headers a frozenset so it will be hashable
self.headers = frozenset(headers.items())
self.uri = uri
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):
warnings.warn("Request.add_header is deprecated. "
"Please assign to request.headers instead.",
DeprecationWarning)
self.headers[key] = value
@property
def scheme(self):
return urlparse(self.uri).scheme
@property
def host(self):
return urlparse(self.uri).hostname
@property
def port(self):
parse_uri = urlparse(self.uri)
port = parse_uri.port
if port is None:
port = {'https': 443, 'http': 80}[parse_uri.scheme]
return port
@property
def path(self):
return urlparse(self.uri).path
@property
def query(self):
q = urlparse(self.uri).query
return sorted(parse_qsl(q))
# alias for backwards compatibility
@property
def url(self):
return "{0}://{1}{2}".format(self.protocol, self.host, self.path)
return self.uri
def __key(self):
return (
self.host,
self.port,
self.method,
self.path,
self.body,
self.headers
)
def __hash__(self):
return hash(self.__key())
def __eq__(self, other):
return hash(self) == hash(other)
# alias for backwards compatibility
@property
def protocol(self):
return self.scheme
def __str__(self):
return "<Request ({0}) {1}>".format(self.method, self.url)
return "<Request ({0}) {1}>".format(self.method, self.uri)
def __repr__(self):
return self.__str__()
def _to_dict(self):
return {
'protocol': self.protocol,
'host': self.host,
'port': self.port,
'method': self.method,
'path': self.path,
'uri': self.uri,
'body': self.body,
'headers': self.headers,
'headers': dict(((k, [v]) for k, v in self.headers.items())),
}
@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)

63
vcr/serialize.py Normal file
View File

@@ -0,0 +1,63 @@
from vcr.serializers import compat
from vcr.request import Request
import yaml
# version 1 cassettes started with VCR 1.0.x.
# Before 1.0.x, there was no versioning.
CASSETTE_FORMAT_VERSION = 1
"""
Just a general note on the serialization philosophy here:
I prefer cassettes to be human-readable if possible. Yaml serializes
bytestrings to !!binary, which isn't readable, so I would like to serialize to
strings and from strings, which yaml will encode as utf-8 automatically.
All the internal HTTP stuff expects bytestrings, so this whole serialization
process feels backwards.
Serializing: bytestring -> string (yaml persists to utf-8)
Deserializing: string (yaml converts from utf-8) -> bytestring
"""
def _looks_like_an_old_cassette(data):
return isinstance(data, list) and len(data) and 'request' in data[0]
def _warn_about_old_cassette_format():
raise ValueError(
"Your cassette files were generated in an older version "
"of VCR. Delete your cassettes or run the migration script."
"See http://git.io/mHhLBg for more details."
)
def deserialize(cassette_string, serializer):
try:
data = serializer.deserialize(cassette_string)
# Old cassettes used to use yaml object thingy so I have to
# check for some fairly stupid exceptions here
except (ImportError, yaml.constructor.ConstructorError):
_warn_about_old_cassette_format()
if _looks_like_an_old_cassette(data):
_warn_about_old_cassette_format()
requests = [Request._from_dict(r['request']) for r in data['interactions']]
responses = [
compat.convert_to_bytes(r['response']) for r in data['interactions']
]
return requests, responses
def serialize(cassette_dict, serializer):
interactions = ([{
'request': compat.convert_to_unicode(request._to_dict()),
'response': compat.convert_to_unicode(response),
} for request, response in zip(
cassette_dict['requests'],
cassette_dict['responses'],
)])
data = {
'version': CASSETTE_FORMAT_VERSION,
'interactions': interactions,
}
return serializer.serialize(data)

79
vcr/serializers/compat.py Normal file
View File

@@ -0,0 +1,79 @@
import six
def convert_to_bytes(resp):
resp = convert_body_to_bytes(resp)
return resp
def convert_to_unicode(resp):
resp = convert_body_to_unicode(resp)
return resp
def convert_body_to_bytes(resp):
"""
If the request body is a string, encode it to bytes (for python3 support)
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
that's what all the internal httplib machinery is expecting.
For more info on py3 yaml:
http://pyyaml.org/wiki/PyYAMLDocumentation#Python3support
"""
try:
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
# have the keys we were expecting. Some of the tests just serialize
# and deserialize a string.
# Also, sometimes the thing actually is binary, so if you can't encode
# it, just give up.
pass
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 or responses body is bytes, decode it to a string
(for python3 support)
"""
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)
return resp

View File

@@ -1,34 +1,29 @@
from vcr.request import Request
try:
import simplejson as json
except ImportError:
import json
def _json_default(obj):
if isinstance(obj, frozenset):
return dict(obj)
return obj
def _fix_response_unicode(d):
d['body']['string'] = d['body']['string'].encode('utf-8')
return d
def deserialize(cassette_string):
data = json.loads(cassette_string)
requests = [Request._from_dict(r['request']) for r in data]
responses = [_fix_response_unicode(r['response']) for r in data]
return requests, responses
return json.loads(cassette_string)
def serialize(cassette_dict):
data = ([{
'request': request._to_dict(),
'response': response,
} for request, response in zip(
cassette_dict['requests'],
cassette_dict['responses']
)])
return json.dumps(data, indent=4, default=_json_default)
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: # py2
raise UnicodeDecodeError(
original.encoding,
b"Error serializing cassette to JSON",
original.start,
original.end,
original.args[-1] + error_message
)
except TypeError as original: # py3
raise TypeError(error_message)

View File

@@ -8,18 +8,8 @@ except ImportError:
def deserialize(cassette_string):
data = yaml.load(cassette_string, Loader=Loader)
requests = [r['request'] for r in data]
responses = [r['response'] for r in data]
return requests, responses
return yaml.load(cassette_string, Loader=Loader)
def serialize(cassette_dict):
data = ([{
'request': request,
'response': response,
} for request, response in zip(
cassette_dict['requests'],
cassette_dict['responses']
)])
return yaml.dump(data, Dumper=Dumper)
return yaml.dump(cassette_dict, Dumper=Dumper)

View File

@@ -1,172 +1,258 @@
'''Stubs for patching HTTP and HTTPS requests'''
from httplib import HTTPConnection, HTTPSConnection, HTTPMessage
from cStringIO import StringIO
import logging
import six
from six.moves.http_client import (
HTTPConnection,
HTTPSConnection,
HTTPResponse,
)
from six import BytesIO
from vcr.request import Request
from vcr.errors import CannotOverwriteExistingCassetteException
from . import compat
log = logging.getLogger(__name__)
class VCRHTTPResponse(object):
class VCRFakeSocket(object):
"""
A socket that doesn't do anything!
Used when playing back casssettes, when there
is no actual open socket.
"""
def close(self):
pass
def settimeout(self, *args, **kwargs):
pass
def fileno(self):
"""
This is kinda crappy. requests will watch
this descriptor and make sure it's not closed.
Return file descriptor 0 since that's stdin.
"""
return 0 # wonder how bad this is....
def parse_headers(header_list):
"""
Convert headers from our serialized dict with lists for keys to a
HTTPMessage
"""
header_string = b""
for key, values in header_list.items():
for v in values:
header_string += \
key.encode('utf-8') + b":" + v.encode('utf-8') + b"\r\n"
return compat.get_httpmessage(header_string)
def serialize_headers(response):
out = {}
for key, values in compat.get_headers(response.msg):
out.setdefault(key, [])
out[key].extend(values)
return out
class VCRHTTPResponse(HTTPResponse):
"""
Stub reponse class that gets returned instead of a HTTPResponse
"""
def __init__(self, recorded_response):
self.recorded_response = recorded_response
self.reason = recorded_response['status']['message']
self.status = recorded_response['status']['code']
self.status = self.code = recorded_response['status']['code']
self.version = None
self._content = StringIO(self.recorded_response['body']['string'])
self._content = BytesIO(self.recorded_response['body']['string'])
self._closed = False
# We are skipping the header parsing (they have already been parsed
# at this point) and directly adding the headers to the header
# container, so just pass an empty StringIO.
self.msg = HTTPMessage(StringIO(''))
headers = self.recorded_response['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)
for key, val in self.recorded_response['headers'].iteritems():
self.msg.addheader(key, val)
# msg.addheaders adds the headers to msg.dict, but not to
# the msg.headers list representation of headers, so
# I have to add it to both.
self.msg.headers.append("{0}:{1}".format(key, val))
self.length = compat.get_header(self.msg, 'content-length') or None
self.length = self.msg.getheader('content-length') or None
@property
def closed(self):
# in python3, I can't change the value of self.closed. So I'
# twiddling self._closed and using this property to shadow the real
# self.closed from the superclas
return self._closed
def read(self, *args, **kwargs):
# Note: I'm pretty much ignoring any chunking stuff because
# I don't really understand what it is or how it works.
return self._content.read(*args, **kwargs)
def readline(self, *args, **kwargs):
return self._content.readline(*args, **kwargs)
def close(self):
self._closed = True
return True
def getcode(self):
return self.status
def isclosed(self):
# Urllib3 seems to call this because it actually uses
# the weird chunking support in httplib
return True
return self.closed
def info(self):
return parse_headers(self.recorded_response['headers'])
def getheaders(self):
return self.recorded_response['headers'].iteritems()
message = parse_headers(self.recorded_response['headers'])
return list(compat.get_header_items(message))
def getheader(self, header, default=None):
values = [v for (k, v) in self.getheaders() if k.lower() == header.lower()]
if values:
return ', '.join(values)
else:
return default
class VCRConnectionMixin:
class VCRConnection(object):
# A reference to the cassette that's currently being patched in
cassette = None
def request(self, method, url, body=None, headers=None):
def _port_postfix(self):
"""
Returns empty string for the default port and ':port' otherwise
"""
port = self.real_connection.port
default_port = {'https': 443, 'http': 80}[self._protocol]
return ':{0}'.format(port) if port != default_port else ''
def _uri(self, url):
"""Returns request absolute URI"""
uri = "{0}://{1}{2}{3}".format(
self._protocol,
self.real_connection.host,
self._port_postfix(),
url,
)
return uri
def _url(self, uri):
"""Returns request selector url from absolute URI"""
prefix = "{0}://{1}{2}".format(
self._protocol,
self.real_connection.host,
self._port_postfix(),
)
return uri.replace(prefix, '', 1)
def request(self, method, url, body=None, headers=None, *args, **kwargs):
'''Persist the request metadata in self._vcr_request'''
self._vcr_request = Request(
protocol=self._protocol,
host=self.host,
port=self.port,
method=method,
path=url,
uri=self._uri(url),
body=body,
headers=headers or {}
)
log.debug('Got {0}'.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.
def putrequest(self, method, url, *args, **kwargs):
"""
httplib gives you more than one way to do it. This is a way
to start building up a request. Usually followed by a bunch
of putheader() calls.
"""
self._vcr_request = Request(
method=method,
uri=self._uri(url),
body="",
headers={}
)
log.debug('Got {0}'.format(self._vcr_request))
def putheader(self, header, *values):
self._vcr_request.headers[header] = values
def send(self, data):
'''
This method is called after request(), to add additional data to the
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 _send_request(self, method, url, body, headers):
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, message_body=None):
"""
Copy+pasted from python stdlib 2.6 source because it
has a call to self.send() which I have overridden
#stdlibproblems #fml
Normally, this would actually send the request to the server.
We are not sending the request until getting the response,
so bypass this part and just append the message body, if any.
"""
header_names = dict.fromkeys([k.lower() for k in headers])
skips = {}
if 'host' in header_names:
skips['skip_host'] = 1
if 'accept-encoding' in header_names:
skips['skip_accept_encoding'] = 1
self.putrequest(method, url, **skips)
if body and ('content-length' not in header_names):
thelen = None
try:
thelen = str(len(body))
except TypeError, te:
# If this is a file-like object, try to
# fstat its file descriptor
import os
try:
thelen = str(os.fstat(body.fileno()).st_size)
except (AttributeError, OSError):
# Don't send a length if this failed
if self.debuglevel > 0:
print "Cannot stat!!"
if thelen is not None:
self.putheader('Content-Length', thelen)
for hdr, value in headers.iteritems():
self.putheader(hdr, value)
self.endheaders()
if body:
self._baseclass.send(self, body)
def _send_output(self, message_body=None):
"""
Copy-and-pasted from httplib, just so I can modify the self.send()
calls to call the superclass's send(), since I had to override the
send() behavior, since send() is both an external and internal
httplib API.
"""
self._buffer.extend(("", ""))
msg = "\r\n".join(self._buffer)
del self._buffer[:]
# If msg and message_body are sent in a single send() call,
# it will avoid performance problems caused by the interaction
# between delayed ack and the Nagle algorithm.
if isinstance(message_body, str):
msg += message_body
message_body = None
self._baseclass.send(self, msg)
if message_body is not None:
#message_body was not a string (i.e. it is a file) and
#we must run the risk of Nagle
self._baseclass.send(self, message_body)
self._vcr_request.body = message_body
def getresponse(self, _=False):
'''Retrieve a the response'''
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._vcr_request in self.cassette and \
self.cassette.record_mode != "all":
response = self.cassette.response_of(self._vcr_request)
# Alert the cassette to the fact that we've served another
# response for the provided requests
self.cassette.mark_played(self._vcr_request)
if self.cassette.can_play_response_for(self._vcr_request):
log.info(
"Playing response for {0} from cassette".format(
self._vcr_request
)
)
response = self.cassette.play_response(self._vcr_request)
return VCRHTTPResponse(response)
else:
if self.cassette.write_protected:
raise Exception("cassette is write protected")
if self.cassette.write_protected and self.cassette.filter_request(
self._vcr_request
):
raise CannotOverwriteExistingCassetteException(
"No match for the request (%r) was found. "
"Can't overwrite existing cassette (%r) in "
"your current record mode (%r)."
% (self._vcr_request, self.cassette._path,
self.cassette.record_mode)
)
# Otherwise, we should send the request, then get the response
# and return it.
# make the request
self._baseclass.request(
self,
method=self._vcr_request.method,
url=self._vcr_request.path,
body=self._vcr_request.body,
headers=dict(self._vcr_request.headers or {})
log.info(
"{0} not in cassette, sending to real server".format(
self._vcr_request
)
)
# 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._baseclass.getresponse(self)
response = self.real_connection.getresponse()
# put the response into the cassette
response = {
@@ -174,32 +260,94 @@ class VCRConnectionMixin:
'code': response.status,
'message': response.reason
},
'headers': dict(response.getheaders()),
'headers': serialize_headers(response),
'body': {'string': response.read()},
}
self.cassette.append(self._vcr_request, response)
return VCRHTTPResponse(response)
def set_debuglevel(self, *args, **kwargs):
self.real_connection.set_debuglevel(*args, **kwargs)
class VCRHTTPConnection(VCRConnectionMixin, HTTPConnection):
def connect(self, *args, **kwargs):
"""
httplib2 uses this. Connects to the server I'm assuming.
Only pass to the baseclass if we don't have a recorded response
and are not write-protected.
"""
if hasattr(self, '_vcr_request') and \
self.cassette.can_play_response_for(self._vcr_request):
# We already have a response we are going to play, don't
# actually connect
return
if self.cassette.write_protected:
# Cassette is write-protected, don't actually connect
return
from vcr.patch import force_reset
with force_reset():
return self.real_connection.connect(*args, **kwargs)
@property
def sock(self):
if self.real_connection.sock:
return self.real_connection.sock
return VCRFakeSocket()
@sock.setter
def sock(self, value):
if self.real_connection.sock:
self.real_connection.sock = value
def __init__(self, *args, **kwargs):
if six.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
# the reset if you want to see what I mean :)
from vcr.patch import force_reset
with force_reset():
self.real_connection = self._baseclass(*args, **kwargs)
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)
for k, v in HTTPConnection.__dict__.items():
if isinstance(v, staticmethod):
setattr(VCRConnection, k, v)
class VCRHTTPConnection(VCRConnection):
'''A Mocked class for HTTP requests'''
# Can't use super since this is an old-style class
_baseclass = HTTPConnection
_protocol = 'http'
def __init__(self, *args, **kwargs):
HTTPConnection.__init__(self, *args, **kwargs)
class VCRHTTPSConnection(VCRConnectionMixin, HTTPSConnection):
class VCRHTTPSConnection(VCRConnection):
'''A Mocked class for HTTPS requests'''
_baseclass = HTTPSConnection
_protocol = 'https'
def __init__(self, *args, **kwargs):
'''I overrode the init and copied a lot of the code from the parent
class because HTTPConnection when this happens has been replaced by
VCRHTTPConnection, but doing it here lets us use the original one.'''
HTTPConnection.__init__(self, *args, **kwargs)
self.key_file = kwargs.pop('key_file', None)
self.cert_file = kwargs.pop('cert_file', None)
is_verified = True

View File

@@ -0,0 +1,81 @@
'''Stubs for aiohttp HTTP clients'''
from __future__ import absolute_import
import asyncio
import functools
import json
from aiohttp import ClientResponse
from yarl import URL
from vcr.request import Request
class MockClientResponse(ClientResponse):
# TODO: get encoding from header
@asyncio.coroutine
def json(self, *, encoding='utf-8', loads=json.loads): # NOQA: E999
return loads(self.content.decode(encoding))
@asyncio.coroutine
def text(self, encoding='utf-8'):
return self.content.decode(encoding)
@asyncio.coroutine
def release(self):
pass
def vcr_request(cassette, real_request):
@functools.wraps(real_request)
@asyncio.coroutine
def new_request(self, method, url, **kwargs):
headers = kwargs.get('headers')
headers = self._prepare_headers(headers)
data = kwargs.get('data')
params = kwargs.get('params')
if params:
for k, v in params.items():
params[k] = str(v)
request_url = URL(url).with_query(params)
vcr_request = Request(method, str(request_url), data, headers)
if cassette.can_play_response_for(vcr_request):
vcr_response = cassette.play_response(vcr_request)
response = MockClientResponse(method, URL(vcr_response.get('url')))
response.status = vcr_response['status']['code']
response.content = vcr_response['body']['string']
response.reason = vcr_response['status']['message']
response.headers = vcr_response['headers']
response.close()
return response
if cassette.write_protected and cassette.filter_request(vcr_request):
response = MockClientResponse(method, URL(url))
response.status = 599
msg = ("No match for the request {!r} was found. Can't overwrite "
"existing cassette {!r} in your current record mode {!r}.")
msg = msg.format(vcr_request, cassette._path, cassette.record_mode)
response.content = msg.encode()
response.close()
return response
response = yield from real_request(self, method, url, **kwargs) # NOQA: E999
vcr_response = {
'status': {
'code': response.status,
'message': response.reason,
},
'headers': dict(response.headers),
'body': {'string': (yield from response.text())}, # NOQA: E999
'url': response.url,
}
cassette.append(vcr_request, vcr_response)
return response
return new_request

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

8
vcr/stubs/boto_stubs.py Normal file
View File

@@ -0,0 +1,8 @@
'''Stubs for boto'''
from boto.https_connection import CertValidatingHTTPSConnection
from ..stubs import VCRHTTPSConnection
class VCRCertValidatingHTTPSConnection(VCRHTTPSConnection):
_baseclass = CertValidatingHTTPSConnection

43
vcr/stubs/compat.py Normal file
View File

@@ -0,0 +1,43 @@
import six
from six import BytesIO
from six.moves.http_client import HTTPMessage
try:
import http.client
except ImportError:
pass
"""
The python3 http.client api moved some stuff around, so this is an abstraction
layer that tries to cope with this move.
"""
def get_header(message, name):
if six.PY3:
return message.getallmatchingheaders(name)
else:
return message.getheader(name)
def get_header_items(message):
for (key, values) in get_headers(message):
for value in values:
yield key, value
def get_headers(message):
for key in set(message.keys()):
if six.PY3:
yield key, message.get_all(key)
else:
yield key, message.getheaders(key)
def get_httpmessage(headers):
if six.PY3:
return http.client.parse_headers(BytesIO(headers))
msg = HTTPMessage(BytesIO(headers))
msg.fp.seek(0)
msg.readheaders()
return msg

View File

@@ -0,0 +1,63 @@
'''Stubs for httplib2'''
from httplib2 import HTTPConnectionWithTimeout, HTTPSConnectionWithTimeout
from ..stubs import VCRHTTPConnection, VCRHTTPSConnection
class VCRHTTPConnectionWithTimeout(VCRHTTPConnection,
HTTPConnectionWithTimeout):
_baseclass = HTTPConnectionWithTimeout
def __init__(self, *args, **kwargs):
'''I overrode the init because I need to clean kwargs before calling
HTTPConnection.__init__.'''
# Delete the keyword arguments that HTTPConnection would not recognize
safe_keys = set(
('host', 'port', 'strict', 'timeout', 'source_address')
)
unknown_keys = set(kwargs.keys()) - safe_keys
safe_kwargs = kwargs.copy()
for kw in unknown_keys:
del safe_kwargs[kw]
self.proxy_info = kwargs.pop('proxy_info', None)
VCRHTTPConnection.__init__(self, *args, **safe_kwargs)
self.sock = self.real_connection.sock
class VCRHTTPSConnectionWithTimeout(VCRHTTPSConnection,
HTTPSConnectionWithTimeout):
_baseclass = HTTPSConnectionWithTimeout
def __init__(self, *args, **kwargs):
# Delete the keyword arguments that HTTPSConnection would not recognize
safe_keys = set((
'host',
'port',
'key_file',
'cert_file',
'strict',
'timeout',
'source_address',
'ca_certs',
))
unknown_keys = set(kwargs.keys()) - safe_keys
safe_kwargs = kwargs.copy()
for kw in unknown_keys:
del safe_kwargs[kw]
self.proxy_info = kwargs.pop('proxy_info', None)
if 'ca_certs' not in kwargs or kwargs['ca_certs'] is None:
try:
import httplib2
self.ca_certs = httplib2.CA_CERTS
except ImportError:
self.ca_certs = None
else:
self.ca_certs = kwargs['ca_certs']
self.disable_ssl_certificate_validation = kwargs.pop(
'disable_ssl_certificate_validation', None)
VCRHTTPSConnection.__init__(self, *args, **safe_kwargs)
self.sock = self.real_connection.sock

View File

@@ -1,8 +1,19 @@
'''Stubs for requests'''
from requests.packages.urllib3.connectionpool import VerifiedHTTPSConnection
from ..stubs import VCRHTTPSConnection
try:
from requests.packages.urllib3.connectionpool import HTTPConnection, VerifiedHTTPSConnection
except ImportError:
from 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 VCRVerifiedHTTPSConnection(VCRHTTPSConnection, VerifiedHTTPSConnection):
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