1
0
mirror of https://github.com/kevin1024/vcrpy.git synced 2025-12-09 17:15:35 +00:00

Compare commits

...

250 Commits

Author SHA1 Message Date
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
61 changed files with 3426 additions and 846 deletions

2
.gitignore vendored
View File

@@ -6,3 +6,5 @@ dist/
.coverage .coverage
*.egg-info/ *.egg-info/
pytestdebug.log pytestdebug.log
fixtures/

View File

@@ -1,13 +1,40 @@
language: python language: python
before_install: openssl version before_install: openssl version
env: env:
- WITH_REQUESTS="True" global:
- WITH_REQUESTS="False" - secure: AifoKzwhjV94cmcQZrdQmqRu/9rkZZvWpwBv1daeAQpLOKFPGsOm3D+x2cSw9+iCfkgDZDfqQVv1kCaFVxTll8v8jTq5SJdqEY0NmGWbj/UkNtShh609oRDsuzLxAEwtVKYjf/h8K2BRea+bl1tGkwZ2vtmYS6dxNlAijjWOfds=
- secure: LBSEg/gMj4u4Hrpo3zs6Y/1mTpd2RtcN49mZIFgTdbJ9IhpiNPqcEt647Lz94F9Eses2x2WbNuKqZKZZReY7QLbEzU1m0nN5jlaKrjcG5NR5clNABfFFyhgc0jBikyS4abAG8jc2efeaTrFuQwdoF4sE8YiVrkiVj2X5Xoi6sBk=
matrix:
- WITH_LIB="requests2.2"
- WITH_LIB="requests2.3"
- WITH_LIB="requests2.4"
- WITH_LIB="requests2.5"
- WITH_LIB="requests1.x"
- WITH_LIB="httplib2"
- WITH_LIB="boto"
matrix:
allow_failures:
- env: WITH_LIB="boto"
exclude:
- env: WITH_LIB="boto"
python: 3.3
- env: WITH_LIB="boto"
python: 3.4
- env: WITH_LIB="requests1.x"
python: 3.4
python: python:
- 2.6 - 2.6
- 2.7 - 2.7
- pypy - 3.3
install: - 3.4
- pip install PyYAML pytest --use-mirrors - pypy
- if [ $WITH_REQUESTS = "True" ] ; then pip install requests; fi install:
script: python setup.py test - pip install PyYAML pytest pytest-localserver --use-mirrors
- if [ $WITH_LIB = "requests1.x" ] ; then pip install requests==1.2.3; fi
- if [ $WITH_LIB = "requests2.2" ] ; then pip install requests==2.2.1; fi
- if [ $WITH_LIB = "requests2.3" ] ; then pip install requests==2.3.0; fi
- if [ $WITH_LIB = "requests2.4" ] ; then pip install requests==2.4.0; fi
- if [ $WITH_LIB = "requests2.5" ] ; then pip install requests==2.5.0; fi
- if [ $WITH_LIB = "httplib2" ] ; then pip install httplib2; fi
- if [ $WITH_LIB = "boto" ] ; then pip install boto; fi
script: python setup.py test

View File

@@ -1,4 +1,4 @@
Copyright (c) 2012-2013 Kevin McCarthy Copyright (c) 2012-2014 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: 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:

529
README.md
View File

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

View File

@@ -13,41 +13,46 @@ class PyTest(TestCommand):
self.test_suite = True self.test_suite = True
def run_tests(self): def run_tests(self):
#import here, cause outside the eggs aren't loaded # import here, cause outside the eggs aren't loaded
import pytest import pytest
errno = pytest.main(self.test_args) errno = pytest.main(self.test_args)
sys.exit(errno) sys.exit(errno)
setup(name='vcrpy', setup(
version='0.3.4', name='vcrpy',
description="A Python port of Ruby's VCR to make mocking HTTP easier", version='1.1.3',
author='Kevin McCarthy', description=(
author_email='me@kevinmccarthy.org', "Automatically mock your HTTP interactions to simplify and "
url='https://github.com/kevin1024/vcrpy', "speed up testing"
packages = [ ),
author='Kevin McCarthy',
author_email='me@kevinmccarthy.org',
url='https://github.com/kevin1024/vcrpy',
packages=[
'vcr', 'vcr',
'vcr.stubs', 'vcr.stubs',
'vcr.compat', 'vcr.compat',
'vcr.persisters', 'vcr.persisters',
'vcr.serializers', 'vcr.serializers',
], ],
package_dir={ package_dir={
'vcr': 'vcr', 'vcr': 'vcr',
'vcr.stubs': 'vcr/stubs', 'vcr.stubs': 'vcr/stubs',
'vcr.compat': 'vcr/compat', 'vcr.compat': 'vcr/compat',
'vcr.persisters': 'vcr/persisters', 'vcr.persisters': 'vcr/persisters',
}, },
install_requires=['PyYAML'], install_requires=['PyYAML', 'mock', 'six', 'contextlib2', 'wrapt'],
license='MIT', license='MIT',
tests_require=['pytest','mock'], tests_require=['pytest', 'mock', 'pytest-localserver'],
cmdclass={'test': PyTest}, cmdclass={'test': PyTest},
classifiers=[ classifiers=[
'Development Status :: 4 - Beta', 'Development Status :: 4 - Beta',
'Environment :: Console', 'Environment :: Console',
'Intended Audience :: Developers', 'Intended Audience :: Developers',
'Programming Language :: Python', 'Programming Language :: Python',
'Topic :: Software Development :: Testing', 'Programming Language :: Python :: 3',
'Topic :: Internet :: WWW/HTTP', 'Topic :: Software Development :: Testing',
'License :: OSI Approved :: MIT License', 'Topic :: Internet :: WWW/HTTP',
], 'License :: OSI Approved :: MIT License',
],
) )

View File

@@ -1,3 +1,6 @@
import json
def assert_cassette_empty(cass): def assert_cassette_empty(cass):
assert len(cass) == 0 assert len(cass) == 0
assert cass.play_count == 0 assert cass.play_count == 0
@@ -6,3 +9,11 @@ def assert_cassette_empty(cass):
def assert_cassette_has_one_response(cass): def assert_cassette_has_one_response(cass):
assert len(cass) == 1 assert len(cass) == 1
assert cass.play_count == 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 body: null
headers: !!python/object/apply:__builtin__.frozenset headers:
- - !!python/tuple [Accept-Encoding, 'gzip, deflate, compress'] Accept: ['*/*']
- !!python/tuple [User-Agent, vcrpy-test] Accept-Encoding: ['gzip, deflate, compress']
- !!python/tuple [Accept, '*/*'] User-Agent: ['vcrpy-test']
host: moz.com
method: GET method: GET
path: / uri: http://seomoz.org/
port: 80 response:
protocol: http 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: response:
body: body:
string: !!binary | string: !!binary |
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
headers: {accept-ranges: bytes, age: '3593', cache-control: 'no-cache, must-revalidate, tmrTWRTxFtkRz8IVbD0iP5GtypwqzeEy4RgoaPfW42ZKhTyjCSur6RQH1+d0gnK3nNg77f1jUoDW
s-maxage=3600', connection: keep-alive, content-encoding: gzip, content-length: '5300', ol79Cgmbiqed8vKAQUjItie0RwrO6L8ebaNitXWIG69P6BEh1M/TOIZVnV9EPpDW/fW1MzgxT8+P
content-type: text/html, date: 'Sat, 10 Aug 2013 23:33:35 GMT', expires: 'Fri, D35+cXZ4+vSZ9fZkcNx9vOybRVdV36OjZ788O31+evzm6OD8xcnv9rOfX5sG79usJ6kWFOeexn6a
15 Oct 2004 12:00:00 GMT', server: nginx, server-name: dalmozwww02.dal.moz.com, YOUbBi+ixE8XqnjGEa0G6LAUCCEP5OKiIgA7VDUXwVfBWC+/XVi61ZQfSQNC/J8PQAK2K6fYvVsE
vary: Accept-Encoding, via: 1.1 varnish, x-varnish: 2027569899 2027508286} 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} 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

@@ -1,9 +1,9 @@
'''Basic tests about cassettes''' '''Basic tests for cassettes'''
# coding=utf-8 # coding=utf-8
# External imports # External imports
import os import os
import urllib2 from six.moves.urllib.request import urlopen
# Internal imports # Internal imports
import vcr import vcr
@@ -16,7 +16,7 @@ def test_nonexistent_directory(tmpdir):
# Run VCR to create dir and cassette file # Run VCR to create dir and cassette file
with vcr.use_cassette(str(tmpdir.join('nonexistent', 'cassette.yml'))): with vcr.use_cassette(str(tmpdir.join('nonexistent', 'cassette.yml'))):
urllib2.urlopen('http://httpbin.org/').read() urlopen('http://httpbin.org/').read()
# This should have made the file and the directory # This should have made the file and the directory
assert os.path.exists(str(tmpdir.join('nonexistent', 'cassette.yml'))) assert os.path.exists(str(tmpdir.join('nonexistent', 'cassette.yml')))
@@ -25,11 +25,11 @@ def test_nonexistent_directory(tmpdir):
def test_unpatch(tmpdir): def test_unpatch(tmpdir):
'''Ensure that our cassette gets unpatched when we're done''' '''Ensure that our cassette gets unpatched when we're done'''
with vcr.use_cassette(str(tmpdir.join('unpatch.yaml'))) as cass: with vcr.use_cassette(str(tmpdir.join('unpatch.yaml'))) as cass:
urllib2.urlopen('http://httpbin.org/').read() urlopen('http://httpbin.org/').read()
# Make the same request, and assert that we haven't served any more # Make the same request, and assert that we haven't served any more
# requests out of cache # requests out of cache
urllib2.urlopen('http://httpbin.org/').read() urlopen('http://httpbin.org/').read()
assert cass.play_count == 0 assert cass.play_count == 0
@@ -38,10 +38,10 @@ def test_basic_use(tmpdir):
Copied from the docs Copied from the docs
''' '''
with vcr.use_cassette('fixtures/vcr_cassettes/synopsis.yaml'): with vcr.use_cassette('fixtures/vcr_cassettes/synopsis.yaml'):
response = urllib2.urlopen( response = urlopen(
'http://www.iana.org/domains/reserved' 'http://www.iana.org/domains/reserved'
).read() ).read()
assert 'Example domains' in response assert b'Example domains' in response
def test_basic_json_use(tmpdir): def test_basic_json_use(tmpdir):
@@ -50,8 +50,8 @@ def test_basic_json_use(tmpdir):
''' '''
test_fixture = 'fixtures/vcr_cassettes/synopsis.json' test_fixture = 'fixtures/vcr_cassettes/synopsis.json'
with vcr.use_cassette(test_fixture, serializer='json'): with vcr.use_cassette(test_fixture, serializer='json'):
response = urllib2.urlopen('http://httpbin.org/').read() response = urlopen('http://httpbin.org/').read()
assert 'difficult sometimes' in response assert b'difficult sometimes' in response
def test_patched_content(tmpdir): def test_patched_content(tmpdir):
@@ -60,16 +60,16 @@ def test_patched_content(tmpdir):
request request
''' '''
with vcr.use_cassette(str(tmpdir.join('synopsis.yaml'))) as cass: with vcr.use_cassette(str(tmpdir.join('synopsis.yaml'))) as cass:
response = urllib2.urlopen('http://httpbin.org/').read() response = urlopen('http://httpbin.org/').read()
assert cass.play_count == 0 assert cass.play_count == 0
with vcr.use_cassette(str(tmpdir.join('synopsis.yaml'))) as cass: with vcr.use_cassette(str(tmpdir.join('synopsis.yaml'))) as cass:
response2 = urllib2.urlopen('http://httpbin.org/').read() response2 = urlopen('http://httpbin.org/').read()
assert cass.play_count == 1 assert cass.play_count == 1
cass._save(force=True) cass._save(force=True)
with vcr.use_cassette(str(tmpdir.join('synopsis.yaml'))) as cass: with vcr.use_cassette(str(tmpdir.join('synopsis.yaml'))) as cass:
response3 = urllib2.urlopen('http://httpbin.org/').read() response3 = urlopen('http://httpbin.org/').read()
assert cass.play_count == 1 assert cass.play_count == 1
assert response == response2 assert response == response2
@@ -85,16 +85,16 @@ def test_patched_content_json(tmpdir):
testfile = str(tmpdir.join('synopsis.json')) testfile = str(tmpdir.join('synopsis.json'))
with vcr.use_cassette(testfile) as cass: with vcr.use_cassette(testfile) as cass:
response = urllib2.urlopen('http://httpbin.org/').read() response = urlopen('http://httpbin.org/').read()
assert cass.play_count == 0 assert cass.play_count == 0
with vcr.use_cassette(testfile) as cass: with vcr.use_cassette(testfile) as cass:
response2 = urllib2.urlopen('http://httpbin.org/').read() response2 = urlopen('http://httpbin.org/').read()
assert cass.play_count == 1 assert cass.play_count == 1
cass._save(force=True) cass._save(force=True)
with vcr.use_cassette(testfile) as cass: with vcr.use_cassette(testfile) as cass:
response3 = urllib2.urlopen('http://httpbin.org/').read() response3 = urlopen('http://httpbin.org/').read()
assert cass.play_count == 1 assert cass.play_count == 1
assert response == response2 assert response == response2

View File

@@ -0,0 +1,72 @@
import pytest
boto = pytest.importorskip("boto")
import boto
import boto.iam
from boto.s3.connection import S3Connection
from boto.s3.key import Key
from ConfigParser import DuplicateSectionError
import vcr
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'))) as cass:
k = Key(s3_bucket)
k.key = 'test.txt'
k.set_contents_from_string('hello world i am a string')
with vcr.use_cassette(str(tmpdir.join('boto-medium.yml'))) as cass:
k = Key(s3_bucket)
k.key = 'test.txt'
k.set_contents_from_string('hello world i am a string')
def test_boto_hardcore_mode(tmpdir):
with vcr.use_cassette(str(tmpdir.join('boto-hardcore.yml'))) as cass:
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'))) as cass:
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'))) as cass:
iam_conn = boto.iam.connect_to_region('universal')
iam_conn.get_all_users()
with vcr.use_cassette(str(tmpdir.join('boto-iam.yml'))) as cass:
iam_conn = boto.iam.connect_to_region('universal')
iam_conn.get_all_users()

View File

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

View File

@@ -3,8 +3,8 @@
# External imports # External imports
import os import os
import urllib2
import time import time
from six.moves.urllib.request import urlopen
# Internal imports # Internal imports
import vcr import vcr
@@ -17,12 +17,12 @@ def test_disk_saver_nowrite(tmpdir):
''' '''
fname = str(tmpdir.join('synopsis.yaml')) fname = str(tmpdir.join('synopsis.yaml'))
with vcr.use_cassette(fname) as cass: with vcr.use_cassette(fname) as cass:
urllib2.urlopen('http://www.iana.org/domains/reserved').read() urlopen('http://www.iana.org/domains/reserved').read()
assert cass.play_count == 0 assert cass.play_count == 0
last_mod = os.path.getmtime(fname) last_mod = os.path.getmtime(fname)
with vcr.use_cassette(fname) as cass: with vcr.use_cassette(fname) as cass:
urllib2.urlopen('http://www.iana.org/domains/reserved').read() urlopen('http://www.iana.org/domains/reserved').read()
assert cass.play_count == 1 assert cass.play_count == 1
assert cass.dirty is False assert cass.dirty is False
last_mod2 = os.path.getmtime(fname) last_mod2 = os.path.getmtime(fname)
@@ -37,7 +37,7 @@ def test_disk_saver_write(tmpdir):
''' '''
fname = str(tmpdir.join('synopsis.yaml')) fname = str(tmpdir.join('synopsis.yaml'))
with vcr.use_cassette(fname) as cass: with vcr.use_cassette(fname) as cass:
urllib2.urlopen('http://www.iana.org/domains/reserved').read() urlopen('http://www.iana.org/domains/reserved').read()
assert cass.play_count == 0 assert cass.play_count == 0
last_mod = os.path.getmtime(fname) last_mod = os.path.getmtime(fname)
@@ -46,8 +46,8 @@ def test_disk_saver_write(tmpdir):
time.sleep(1) time.sleep(1)
with vcr.use_cassette(fname, record_mode='any') as cass: with vcr.use_cassette(fname, record_mode='any') as cass:
urllib2.urlopen('http://www.iana.org/domains/reserved').read() urlopen('http://www.iana.org/domains/reserved').read()
urllib2.urlopen('http://httpbin.org/').read() urlopen('http://httpbin.org/').read()
assert cass.play_count == 1 assert cass.play_count == 1
assert cass.dirty assert cass.dirty
last_mod2 = os.path.getmtime(fname) last_mod2 = os.path.getmtime(fname)

View File

@@ -0,0 +1,73 @@
import base64
import pytest
from six.moves.urllib.request import urlopen, Request
from six.moves.urllib.error import HTTPError
import vcr
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):
for request in cassette.requests:
for k in request.headers:
if header.lower() == k.lower():
return True
return False
def test_filter_basic_auth(tmpdir):
url = 'http://httpbin.org/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):
url = 'http://httpbin.org/?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_callback(tmpdir):
url = 'http://httpbin.org/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

View File

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

View File

@@ -0,0 +1,55 @@
import base64
import pytest
from six.moves.urllib.request import urlopen, Request
from six.moves.urllib.error import HTTPError
import vcr
def test_ignore_localhost(tmpdir, httpserver):
httpserver.serve_content('Hello!')
cass_file = str(tmpdir.join('filter_qs.yaml'))
with vcr.use_cassette(cass_file, ignore_localhost=True) as cass:
urlopen(httpserver.url)
assert len(cass) == 0
urlopen('http://httpbin.org')
assert len(cass) == 1
def test_ignore_httpbin(tmpdir, httpserver):
httpserver.serve_content('Hello!')
cass_file = str(tmpdir.join('filter_qs.yaml'))
with vcr.use_cassette(
cass_file,
ignore_hosts=['httpbin.org']
) as cass:
urlopen('http://httpbin.org')
assert len(cass) == 0
urlopen(httpserver.url)
assert len(cass) == 1
def test_ignore_localhost_and_httpbin(tmpdir, httpserver):
httpserver.serve_content('Hello!')
cass_file = str(tmpdir.join('filter_qs.yaml'))
with vcr.use_cassette(
cass_file,
ignore_hosts=['httpbin.org'],
ignore_localhost=True
) as cass:
urlopen('http://httpbin.org')
urlopen(httpserver.url)
assert len(cass) == 0
def test_ignore_localhost_twice(tmpdir, httpserver):
httpserver.serve_content('Hello!')
cass_file = str(tmpdir.join('filter_qs.yaml'))
with vcr.use_cassette(cass_file, ignore_localhost=True) as cass:
urlopen(httpserver.url)
assert len(cass) == 0
urlopen('http://httpbin.org')
assert len(cass) == 1
with vcr.use_cassette(cass_file, ignore_localhost=True) as cass:
assert len(cass) == 1
urlopen(httpserver.url)
urlopen('http://httpbin.org')
assert len(cass) == 1

View File

@@ -0,0 +1,99 @@
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
@pytest.fixture
def cassette(tmpdir):
"""
Helper fixture used to prepare the cassete
returns path to the recorded cassette
"""
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'),
('port',
'https://google.com:80/post?a=b',
'http://httpbin.org:5000/get?p1=q1&p2=q2'),
('path',
'https://google.com/get?a=b',
'http://httpbin.org/post?p1=q1&p2=q2'),
('query',
'https://google.com/get?p2=q2&p1=q1',
'http://httpbin.org/get?p1=q1&a=b')
])
def test_matchers(cassette, matcher, matching_uri, not_matching_uri):
# 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):
# 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):
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:5000/get?p1=q1&p2=q2',
'http://httpbin.org/post?p1=q1&p2=q2',
'http://httpbin.org/get?p1=q1&a=b'
])
def test_default_matcher_does_not_match(cassette, uri):
with pytest.raises(vcr.errors.CannotOverwriteExistingCassetteException):
with vcr.use_cassette(cassette):
urlopen(uri)
def test_default_matcher_does_not_match_on_method(cassette):
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):
# make two requests in the first request that are considered
# identical (since the match is based on method)
with vcr.use_cassette(str(tmpdir.join('test.json')), match_on=['method']):
urlopen('http://httpbin.org/status/200')
urlopen('http://httpbin.org/status/201')
# Now, try to make three requests. The first two should return the
# correct status codes in order, and the third should raise an
# exception.
with vcr.use_cassette(str(tmpdir.join('test.json')), match_on=['method']):
assert urlopen('http://httpbin.org/status/200').getcode() == 200
assert urlopen('http://httpbin.org/status/201').getcode() == 201
with pytest.raises(Exception):
urlopen('http://httpbin.org/status/200')

View File

@@ -1,24 +1,46 @@
import os import os
import urllib2
import pytest import pytest
import vcr import vcr
from six.moves.urllib.request import urlopen
def test_once_record_mode(tmpdir): def test_once_record_mode(tmpdir):
testfile = str(tmpdir.join('recordmode.yml')) testfile = str(tmpdir.join('recordmode.yml'))
with vcr.use_cassette(testfile, record_mode="once"): with vcr.use_cassette(testfile, record_mode="once"):
# cassette file doesn't exist, so create. # cassette file doesn't exist, so create.
response = urllib2.urlopen('http://httpbin.org/').read() response = urlopen('http://httpbin.org/').read()
with vcr.use_cassette(testfile, record_mode="once") as cass: with vcr.use_cassette(testfile, record_mode="once") as cass:
# make the same request again # make the same request again
response = urllib2.urlopen('http://httpbin.org/').read() response = urlopen('http://httpbin.org/').read()
# the first time, it's played from the cassette. # the first time, it's played from the cassette.
# but, try to access something else from the same cassette, and an # but, try to access something else from the same cassette, and an
# exception is raised. # exception is raised.
with pytest.raises(Exception): with pytest.raises(Exception):
response = urllib2.urlopen('http://httpbin.org/get').read() response = urlopen('http://httpbin.org/get').read()
def test_once_record_mode_two_times(tmpdir):
testfile = str(tmpdir.join('recordmode.yml'))
with vcr.use_cassette(testfile, record_mode="once"):
# get two of the same file
response1 = urlopen('http://httpbin.org/').read()
response2 = urlopen('http://httpbin.org/').read()
with vcr.use_cassette(testfile, record_mode="once") as cass:
# do it again
response = urlopen('http://httpbin.org/').read()
response = urlopen('http://httpbin.org/').read()
def test_once_mode_three_times(tmpdir):
testfile = str(tmpdir.join('recordmode.yml'))
with vcr.use_cassette(testfile, record_mode="once"):
# get three of the same file
response1 = urlopen('http://httpbin.org/').read()
response2 = urlopen('http://httpbin.org/').read()
response2 = urlopen('http://httpbin.org/').read()
def test_new_episodes_record_mode(tmpdir): def test_new_episodes_record_mode(tmpdir):
@@ -26,34 +48,69 @@ def test_new_episodes_record_mode(tmpdir):
with vcr.use_cassette(testfile, record_mode="new_episodes"): with vcr.use_cassette(testfile, record_mode="new_episodes"):
# cassette file doesn't exist, so create. # cassette file doesn't exist, so create.
response = urllib2.urlopen('http://httpbin.org/').read() response = urlopen('http://httpbin.org/').read()
with vcr.use_cassette(testfile, record_mode="new_episodes") as cass: with vcr.use_cassette(testfile, record_mode="new_episodes") as cass:
# make the same request again # make the same request again
response = urllib2.urlopen('http://httpbin.org/').read() response = urlopen('http://httpbin.org/').read()
# all responses have been played
assert cass.all_played
# in the "new_episodes" record mode, we can add more requests to # in the "new_episodes" record mode, we can add more requests to
# a cassette without repurcussions. # a cassette without repurcussions.
response = urllib2.urlopen('http://httpbin.org/get').read() response = urlopen('http://httpbin.org/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 assert cass.play_count == 1
# not all responses have been played
assert not cass.all_played
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):
testfile = str(tmpdir.join('recordmode.yml'))
url = 'http://httpbin.org/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): def test_all_record_mode(tmpdir):
testfile = str(tmpdir.join('recordmode.yml')) testfile = str(tmpdir.join('recordmode.yml'))
with vcr.use_cassette(testfile, record_mode="all"): with vcr.use_cassette(testfile, record_mode="all"):
# cassette file doesn't exist, so create. # cassette file doesn't exist, so create.
response = urllib2.urlopen('http://httpbin.org/').read() response = urlopen('http://httpbin.org/').read()
with vcr.use_cassette(testfile, record_mode="all") as cass: with vcr.use_cassette(testfile, record_mode="all") as cass:
# make the same request again # make the same request again
response = urllib2.urlopen('http://httpbin.org/').read() response = urlopen('http://httpbin.org/').read()
# in the "all" record mode, we can add more requests to # in the "all" record mode, we can add more requests to
# a cassette without repurcussions. # a cassette without repurcussions.
response = urllib2.urlopen('http://httpbin.org/get').read() response = urlopen('http://httpbin.org/get').read()
# The cassette was never actually played, even though it existed. # The cassette was never actually played, even though it existed.
# that's because, in "all" mode, the requests all go directly to # that's because, in "all" mode, the requests all go directly to
@@ -67,7 +124,7 @@ def test_none_record_mode(tmpdir):
testfile = str(tmpdir.join('recordmode.yml')) testfile = str(tmpdir.join('recordmode.yml'))
with vcr.use_cassette(testfile, record_mode="none"): with vcr.use_cassette(testfile, record_mode="none"):
with pytest.raises(Exception): with pytest.raises(Exception):
response = urllib2.urlopen('http://httpbin.org/').read() response = urlopen('http://httpbin.org/').read()
def test_none_record_mode_with_existing_cassette(tmpdir): def test_none_record_mode_with_existing_cassette(tmpdir):
@@ -75,12 +132,12 @@ def test_none_record_mode_with_existing_cassette(tmpdir):
testfile = str(tmpdir.join('recordmode.yml')) testfile = str(tmpdir.join('recordmode.yml'))
with vcr.use_cassette(testfile, record_mode="all"): with vcr.use_cassette(testfile, record_mode="all"):
response = urllib2.urlopen('http://httpbin.org/').read() response = urlopen('http://httpbin.org/').read()
# play from cassette file # play from cassette file
with vcr.use_cassette(testfile, record_mode="none") as cass: with vcr.use_cassette(testfile, record_mode="none") as cass:
response = urllib2.urlopen('http://httpbin.org/').read() response = urlopen('http://httpbin.org/').read()
assert cass.play_count == 1 assert cass.play_count == 1
# but if I try to hit the net, raise an exception. # but if I try to hit the net, raise an exception.
with pytest.raises(Exception): with pytest.raises(Exception):
response = urllib2.urlopen('http://httpbin.org/get').read() response = urlopen('http://httpbin.org/get').read()

View File

@@ -1,5 +1,5 @@
import urllib2
import vcr import vcr
from six.moves.urllib.request import urlopen
def true_matcher(r1, r2): def true_matcher(r1, r2):
@@ -10,23 +10,27 @@ def false_matcher(r1, r2):
return False return False
def test_registered_serializer_true_matcher(tmpdir): def test_registered_true_matcher(tmpdir):
my_vcr = vcr.VCR() my_vcr = vcr.VCR()
my_vcr.register_matcher('true', true_matcher) my_vcr.register_matcher('true', true_matcher)
testfile = str(tmpdir.join('test.yml')) 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']) as cass:
# These 2 different urls are stored as the same request # These 2 different urls are stored as the same request
urllib2.urlopen('http://httpbin.org/') urlopen('http://httpbin.org/')
urllib2.urlopen('https://httpbin.org/get') urlopen('https://httpbin.org/get')
assert len(cass) == 1
with my_vcr.use_cassette(testfile, match_on=['true']) as cass:
# I can get the response twice even though I only asked for it once
urlopen('http://httpbin.org/get')
urlopen('https://httpbin.org/get')
def test_registered_serializer_false_matcher(tmpdir): def test_registered_false_matcher(tmpdir):
my_vcr = vcr.VCR() my_vcr = vcr.VCR()
my_vcr.register_matcher('false', false_matcher) my_vcr.register_matcher('false', false_matcher)
testfile = str(tmpdir.join('test.yml')) testfile = str(tmpdir.join('test.yml'))
with my_vcr.use_cassette(testfile, match_on=['false']) as cass: with my_vcr.use_cassette(testfile, match_on=['false']) as cass:
# These 2 different urls are stored as different requests # These 2 different urls are stored as different requests
urllib2.urlopen('http://httpbin.org/') urlopen('http://httpbin.org/')
urllib2.urlopen('https://httpbin.org/get') urlopen('https://httpbin.org/get')
assert len(cass) == 2 assert len(cass) == 2

View File

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

View File

@@ -1,11 +1,22 @@
import urllib2
import vcr 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):
with vcr.use_cassette(str(tmpdir.join('test.yml'))) as cass: with vcr.use_cassette(str(tmpdir.join('test.yml'))) as cass:
assert len(cass) == 0 assert len(cass) == 0
urllib2.urlopen('http://httpbin.org/redirect/3') urlopen('http://httpbin.org/redirect/3')
assert cass.requests[0].url == 'http://httpbin.org/redirect/3' assert cass.requests[0].uri == 'http://httpbin.org/redirect/3'
assert cass.requests[3].url == 'http://httpbin.org/get' assert cass.requests[3].uri == 'http://httpbin.org/get'
assert len(cass) == 4 assert len(cass) == 4
def test_records_multiple_header_values(tmpdir, httpserver):
httpserver.serve_content('Hello!', headers=[('foo', 'bar'), ('foo', 'baz')])
with vcr.use_cassette(str(tmpdir.join('test.yml'))) as cass:
assert len(cass) == 0
urlopen(httpserver.url)
assert len(cass) == 1
assert cass.responses[0]['headers']['foo'] == ['bar', 'baz']

View File

@@ -5,7 +5,11 @@
import os import os
import pytest import pytest
import vcr import vcr
from assertions import assert_cassette_empty, assert_cassette_has_one_response from assertions import (
assert_cassette_empty,
assert_cassette_has_one_response,
assert_is_json
)
requests = pytest.importorskip("requests") requests = pytest.importorskip("requests")
@@ -20,49 +24,44 @@ def scheme(request):
def test_status_code(scheme, tmpdir): def test_status_code(scheme, tmpdir):
'''Ensure that we can read the status code''' '''Ensure that we can read the status code'''
url = scheme + '://httpbin.org/' url = scheme + '://httpbin.org/'
with vcr.use_cassette(str(tmpdir.join('atts.yaml'))) as cass: with vcr.use_cassette(str(tmpdir.join('atts.yaml'))):
# Ensure that this is empty to begin with status_code = requests.get(url).status_code
assert_cassette_empty(cass)
assert requests.get(url).status_code == requests.get(url).status_code with vcr.use_cassette(str(tmpdir.join('atts.yaml'))):
# Ensure that we've now cached a single response assert status_code == requests.get(url).status_code
assert_cassette_has_one_response(cass)
def test_headers(scheme, tmpdir): def test_headers(scheme, tmpdir):
'''Ensure that we can read the headers back''' '''Ensure that we can read the headers back'''
url = scheme + '://httpbin.org/' url = scheme + '://httpbin.org/'
with vcr.use_cassette(str(tmpdir.join('headers.yaml'))) as cass: with vcr.use_cassette(str(tmpdir.join('headers.yaml'))):
# Ensure that this is empty to begin with headers = requests.get(url).headers
assert_cassette_empty(cass)
assert requests.get(url).headers == requests.get(url).headers with vcr.use_cassette(str(tmpdir.join('headers.yaml'))):
# Ensure that we've now cached a single response assert headers == requests.get(url).headers
assert_cassette_has_one_response(cass)
def test_body(tmpdir, scheme): def test_body(tmpdir, scheme):
'''Ensure the responses are all identical enough''' '''Ensure the responses are all identical enough'''
url = scheme + '://httpbin.org/bytes/1024' url = scheme + '://httpbin.org/bytes/1024'
with vcr.use_cassette(str(tmpdir.join('body.yaml'))) as cass: with vcr.use_cassette(str(tmpdir.join('body.yaml'))):
# Ensure that this is empty to begin with content = requests.get(url).content
assert_cassette_empty(cass)
assert requests.get(url).content == requests.get(url).content with vcr.use_cassette(str(tmpdir.join('body.yaml'))):
# Ensure that we've now cached a single response assert content == requests.get(url).content
assert_cassette_has_one_response(cass)
def test_auth(tmpdir, scheme): def test_auth(tmpdir, scheme):
'''Ensure that we can handle basic auth''' '''Ensure that we can handle basic auth'''
auth = ('user', 'passwd') auth = ('user', 'passwd')
url = scheme + '://httpbin.org/basic-auth/user/passwd' url = scheme + '://httpbin.org/basic-auth/user/passwd'
with vcr.use_cassette(str(tmpdir.join('auth.yaml'))) as cass: with vcr.use_cassette(str(tmpdir.join('auth.yaml'))):
# Ensure that this is empty to begin with
assert_cassette_empty(cass)
one = requests.get(url, auth=auth) one = requests.get(url, auth=auth)
with vcr.use_cassette(str(tmpdir.join('auth.yaml'))):
two = requests.get(url, auth=auth) two = requests.get(url, auth=auth)
assert one.content == two.content assert one.content == two.content
assert one.status_code == two.status_code 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, scheme):
@@ -76,31 +75,29 @@ def test_auth_failed(tmpdir, scheme):
two = requests.get(url, auth=auth) two = requests.get(url, auth=auth)
assert one.content == two.content assert one.content == two.content
assert one.status_code == two.status_code == 401 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, scheme):
'''Ensure that we can post and cache the results''' '''Ensure that we can post and cache the results'''
data = {'key1': 'value1', 'key2': 'value2'} data = {'key1': 'value1', 'key2': 'value2'}
url = scheme + '://httpbin.org/post' url = scheme + '://httpbin.org/post'
with vcr.use_cassette(str(tmpdir.join('requests.yaml'))) as cass: with vcr.use_cassette(str(tmpdir.join('requests.yaml'))):
# Ensure that this is empty to begin with
assert_cassette_empty(cass)
req1 = requests.post(url, data).content req1 = requests.post(url, data).content
with vcr.use_cassette(str(tmpdir.join('requests.yaml'))):
req2 = requests.post(url, data).content req2 = requests.post(url, data).content
assert req1 == req2
# Ensure that we've now cached a single response assert req1 == req2
assert_cassette_has_one_response(cass)
def test_redirects(tmpdir, scheme): def test_redirects(tmpdir, scheme):
'''Ensure that we can handle redirects''' '''Ensure that we can handle redirects'''
url = scheme + '://httpbin.org/redirect-to?url=bytes/1024' url = scheme + '://httpbin.org/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: with vcr.use_cassette(str(tmpdir.join('requests.yaml'))) as cass:
# Ensure that this is empty to begin with assert content == requests.get(url).content
assert_cassette_empty(cass)
assert requests.get(url).content == requests.get(url).content
# Ensure that we've now cached *two* responses. One for the redirect # Ensure that we've now cached *two* responses. One for the redirect
# and one for the final fetch # and one for the final fetch
assert len(cass) == 2 assert len(cass) == 2
@@ -117,3 +114,94 @@ def test_cross_scheme(tmpdir, scheme):
requests.get('http://httpbin.org/') requests.get('http://httpbin.org/')
assert cass.play_count == 0 assert cass.play_count == 0
assert len(cass) == 2 assert len(cass) == 2
def test_gzip(tmpdir, scheme):
'''
Ensure that requests (actually urllib3) is able to automatically decompress
the response body
'''
url = scheme + '://httpbin.org/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, scheme):
'''
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('http://httpbin.org/get', headers={'Connection': 'close'})
session.get('http://httpbin.org/get', headers={'Connection': 'close'})
def test_https_with_cert_validation_disabled(tmpdir):
with vcr.use_cassette(str(tmpdir.join('cert_validation_disabled.yaml'))):
requests.get('https://httpbin.org', verify=False)
def test_session_can_make_requests_after_requests_unpatched(tmpdir):
with vcr.use_cassette(str(tmpdir.join('test_session_after_unpatched.yaml'))):
session = requests.session()
session.get('http://httpbin.org/get')
with vcr.use_cassette(str(tmpdir.join('test_session_after_unpatched.yaml'))):
session = requests.session()
session.get('http://httpbin.org/get')
session.get('http://httpbin.org/status/200')
def test_session_created_before_use_cassette_is_patched(tmpdir, scheme):
url = scheme + '://httpbin.org/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(scheme + '://httpbin.org/get')
with vcr.use_cassette(str(tmpdir.join('session_created_outside.yaml'))):
# These should only be the same if the patching succeeded.
assert session.get(url).content == body
def test_nested_cassettes_with_session_created_before_nesting(scheme, tmpdir):
'''
This tests ensures that a session that was created while one cassette was
active is patched to the use the responses of a second cassette when it
is enabled.
'''
url = scheme + '://httpbin.org/bytes/1024'
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.
session.get('http://www.reddit.com')

View File

@@ -0,0 +1,42 @@
import vcr
import six.moves.http_client as httplib
def _headers_are_case_insensitive():
conn = httplib.HTTPConnection('httpbin.org')
conn.request('GET', "/cookies/set?k1=v1")
r1 = conn.getresponse()
cookie_data1 = r1.getheader('set-cookie')
conn = httplib.HTTPConnection('httpbin.org')
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):
testfile = str(tmpdir.join('case_insensitivity.yml'))
# check if headers are case insensitive outside of vcrpy
outside = _headers_are_case_insensitive()
with vcr.use_cassette(testfile):
# check if headers are case insensitive inside of vcrpy
inside = _headers_are_case_insensitive()
# check if headers are case insensitive after vcrpy deserializes headers
inside2 = _headers_are_case_insensitive()
# behavior should be the same both inside and outside
assert outside == inside == inside2
def _multiple_header_value(httpserver):
conn = httplib.HTTPConnection('%s:%s' % httpserver.server_address)
conn.request('GET', "/")
r = conn.getresponse()
return r.getheader('foo')
def test_multiple_headers(tmpdir, httpserver):
testfile = str(tmpdir.join('multiple_headers.yaml'))
httpserver.serve_content('Hello!', headers=[('foo', 'bar'), ('foo', 'baz')])
outside = _multiple_header_value(httpserver)
with vcr.use_cassette(testfile):
inside = _multiple_header_value(httpserver)
assert outside == inside

View File

@@ -3,9 +3,10 @@
# External imports # External imports
import os import os
import urllib2
from urllib import urlencode
import pytest import pytest
from six.moves.urllib.request import urlopen
from six.moves.urllib_parse import urlencode
# Internal imports # Internal imports
import vcr import vcr
@@ -25,52 +26,44 @@ def test_response_code(scheme, tmpdir):
'''Ensure we can read a response code from a fetch''' '''Ensure we can read a response code from a fetch'''
url = scheme + '://httpbin.org/' url = scheme + '://httpbin.org/'
with vcr.use_cassette(str(tmpdir.join('atts.yaml'))) as cass: with vcr.use_cassette(str(tmpdir.join('atts.yaml'))) as cass:
# Ensure that this is empty to begin with code = urlopen(url).getcode()
assert_cassette_empty(cass)
assert urllib2.urlopen(url).getcode() == urllib2.urlopen(url).getcode() with vcr.use_cassette(str(tmpdir.join('atts.yaml'))) as cass:
# Ensure that we've now cached a single response assert code == urlopen(url).getcode()
assert_cassette_has_one_response(cass)
def test_random_body(scheme, tmpdir): def test_random_body(scheme, tmpdir):
'''Ensure we can read the content, and that it's served from cache''' '''Ensure we can read the content, and that it's served from cache'''
url = scheme + '://httpbin.org/bytes/1024' url = scheme + '://httpbin.org/bytes/1024'
with vcr.use_cassette(str(tmpdir.join('body.yaml'))) as cass: with vcr.use_cassette(str(tmpdir.join('body.yaml'))) as cass:
# Ensure that this is empty to begin with body = urlopen(url).read()
assert_cassette_empty(cass)
assert urllib2.urlopen(url).read() == urllib2.urlopen(url).read() with vcr.use_cassette(str(tmpdir.join('body.yaml'))) as cass:
# Ensure that we've now cached a single response assert body == urlopen(url).read()
assert_cassette_has_one_response(cass)
def test_response_headers(scheme, tmpdir): def test_response_headers(scheme, tmpdir):
'''Ensure we can get information from the response''' '''Ensure we can get information from the response'''
url = scheme + '://httpbin.org/' url = scheme + '://httpbin.org/'
with vcr.use_cassette(str(tmpdir.join('headers.yaml'))) as cass: with vcr.use_cassette(str(tmpdir.join('headers.yaml'))) as cass:
# Ensure that this is empty to begin with open1 = urlopen(url).info().items()
assert_cassette_empty(cass)
open1 = urllib2.urlopen(url).info().items() with vcr.use_cassette(str(tmpdir.join('headers.yaml'))) as cass:
open2 = urllib2.urlopen(url).info().items() open2 = urlopen(url).info().items()
assert open1 == open2 assert sorted(open1) == sorted(open2)
# Ensure that we've now cached a single response
assert_cassette_has_one_response(cass)
def test_multiple_requests(scheme, tmpdir): def test_multiple_requests(scheme, tmpdir):
'''Ensure that we can cache multiple requests''' '''Ensure that we can cache multiple requests'''
urls = [ urls = [
scheme + '://httpbin.org/',
scheme + '://httpbin.org/', scheme + '://httpbin.org/',
scheme + '://httpbin.org/get', scheme + '://httpbin.org/get',
scheme + '://httpbin.org/bytes/1024' scheme + '://httpbin.org/bytes/1024'
] ]
with vcr.use_cassette(str(tmpdir.join('multiple.yaml'))) as cass: with vcr.use_cassette(str(tmpdir.join('multiple.yaml'))) as cass:
for index in range(len(urls)): [urlopen(url) for url in urls]
url = urls[index] assert len(cass) == len(urls)
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
def test_get_data(scheme, tmpdir): def test_get_data(scheme, tmpdir):
@@ -78,42 +71,38 @@ def test_get_data(scheme, tmpdir):
data = urlencode({'some': 1, 'data': 'here'}) data = urlencode({'some': 1, 'data': 'here'})
url = scheme + '://httpbin.org/get?' + data url = scheme + '://httpbin.org/get?' + data
with vcr.use_cassette(str(tmpdir.join('get_data.yaml'))) as cass: with vcr.use_cassette(str(tmpdir.join('get_data.yaml'))) as cass:
# Ensure that this is empty to begin with res1 = urlopen(url).read()
assert_cassette_empty(cass)
res1 = urllib2.urlopen(url).read() with vcr.use_cassette(str(tmpdir.join('get_data.yaml'))) as cass:
res2 = urllib2.urlopen(url).read() res2 = urlopen(url).read()
assert res1 == res2
# Ensure that we've now cached a single response assert res1 == res2
assert len(cass) == 1
assert cass.play_count == 1
def test_post_data(scheme, tmpdir): def test_post_data(scheme, tmpdir):
'''Ensure that it works when posting data''' '''Ensure that it works when posting data'''
data = urlencode({'some': 1, 'data': 'here'}) data = urlencode({'some': 1, 'data': 'here'}).encode('utf-8')
url = scheme + '://httpbin.org/post' url = scheme + '://httpbin.org/post'
with vcr.use_cassette(str(tmpdir.join('post_data.yaml'))) as cass: with vcr.use_cassette(str(tmpdir.join('post_data.yaml'))) as cass:
# Ensure that this is empty to begin with res1 = urlopen(url, data).read()
assert_cassette_empty(cass)
res1 = urllib2.urlopen(url, data).read() with vcr.use_cassette(str(tmpdir.join('post_data.yaml'))) as cass:
res2 = urllib2.urlopen(url, data).read() res2 = urlopen(url, data).read()
assert res1 == res2
# Ensure that we've now cached a single response assert res1 == res2
assert_cassette_has_one_response(cass) assert_cassette_has_one_response(cass)
def test_post_unicode_data(scheme, tmpdir): def test_post_unicode_data(scheme, tmpdir):
'''Ensure that it works when posting unicode data''' '''Ensure that it works when posting unicode data'''
data = urlencode({'snowman': u''.encode('utf-8')}) data = urlencode({'snowman': u''.encode('utf-8')}).encode('utf-8')
url = scheme + '://httpbin.org/post' url = scheme + '://httpbin.org/post'
with vcr.use_cassette(str(tmpdir.join('post_data.yaml'))) as cass: with vcr.use_cassette(str(tmpdir.join('post_data.yaml'))) as cass:
# Ensure that this is empty to begin with res1 = urlopen(url, data).read()
assert_cassette_empty(cass) with vcr.use_cassette(str(tmpdir.join('post_data.yaml'))) as cass:
res1 = urllib2.urlopen(url, data).read() res2 = urlopen(url, data).read()
res2 = urllib2.urlopen(url, data).read() assert res1 == res2
assert res1 == res2 assert_cassette_has_one_response(cass)
# Ensure that we've now cached a single response
assert_cassette_has_one_response(cass)
def test_cross_scheme(tmpdir): def test_cross_scheme(tmpdir):
@@ -122,7 +111,21 @@ def test_cross_scheme(tmpdir):
# ensure that we haven't served anything out of cache, and we have two # ensure that we haven't served anything out of cache, and we have two
# requests / response pairs in the cassette # requests / response pairs in the cassette
with vcr.use_cassette(str(tmpdir.join('cross_scheme.yaml'))) as cass: with vcr.use_cassette(str(tmpdir.join('cross_scheme.yaml'))) as cass:
urllib2.urlopen('https://httpbin.org/') urlopen('https://httpbin.org/')
urllib2.urlopen('http://httpbin.org/') urlopen('http://httpbin.org/')
assert len(cass) == 2 assert len(cass) == 2
assert cass.play_count == 0 assert cass.play_count == 0
def test_decorator(scheme, tmpdir):
'''Test the decorator version of VCR.py'''
url = scheme + '://httpbin.org/'
@vcr.use_cassette(str(tmpdir.join('atts.yaml')))
def inner1():
return urlopen(url).getcode()
@vcr.use_cassette(str(tmpdir.join('atts.yaml')))
def inner2():
return urlopen(url).getcode()
assert inner1() == inner2()

View File

@@ -3,7 +3,10 @@ requests = pytest.importorskip("requests")
import vcr import vcr
import httplib try:
import httplib
except ImportError:
import http.client as httplib
def test_domain_redirect(): def test_domain_redirect():
@@ -37,11 +40,11 @@ def test_flickr_multipart_upload():
data = r.read() data = r.read()
h.close() h.close()
with vcr.use_cassette('fixtures/vcr_cassettes/flickr.json') as cass: with vcr.use_cassette('fixtures/vcr_cassettes/flickr.yaml') as cass:
_pretend_to_be_flickr_library() _pretend_to_be_flickr_library()
assert len(cass) == 1 assert len(cass) == 1
with vcr.use_cassette('fixtures/vcr_cassettes/flickr.json') as cass: with vcr.use_cassette('fixtures/vcr_cassettes/flickr.yaml') as cass:
assert len(cass) == 1 assert len(cass) == 1
_pretend_to_be_flickr_library() _pretend_to_be_flickr_library()
assert cass.play_count == 1 assert cass.play_count == 1
@@ -52,3 +55,12 @@ def test_flickr_should_respond_with_200(tmpdir):
with vcr.use_cassette(testfile): with vcr.use_cassette(testfile):
r = requests.post("http://api.flickr.com/services/upload") r = requests.post("http://api.flickr.com/services/upload")
assert r.status_code == 200 assert r.status_code == 200
def test_cookies(tmpdir):
testfile = str(tmpdir.join('cookies.yml'))
with vcr.use_cassette(testfile):
s = requests.Session()
r1 = s.get("http://httpbin.org/cookies/set?k1=v1&k2=v2")
r2 = s.get("http://httpbin.org/cookies")
assert len(r2.json()['cookies']) == 2

View File

@@ -1,14 +1,22 @@
import copy
from six.moves import http_client as httplib
import contextlib2
import mock
import pytest import pytest
import yaml import yaml
import mock
from vcr.cassette import Cassette from vcr.cassette import Cassette
from vcr.patch import force_reset
from vcr.errors import UnhandledHTTPRequestError
def test_cassette_load(tmpdir): def test_cassette_load(tmpdir):
a_file = tmpdir.join('test_cassette.yml') a_file = tmpdir.join('test_cassette.yml')
a_file.write(yaml.dump([ a_file.write(yaml.dump({'interactions': [
{'request': 'foo', 'response': 'bar'} {'request': {'body': '', 'uri': 'foo', 'method': 'GET', 'headers': {}},
])) 'response': 'bar'}
]}))
a_cassette = Cassette.load(str(a_file)) a_cassette = Cassette.load(str(a_file))
assert len(a_cassette) == 1 assert len(a_cassette) == 1
@@ -18,21 +26,6 @@ def test_cassette_not_played():
assert not a.play_count 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(): def test_cassette_append():
a = Cassette('test') a = Cassette('test')
a.append('foo', 'bar') a.append('foo', 'bar')
@@ -59,14 +52,132 @@ def test_cassette_contains():
@mock.patch('vcr.cassette.requests_match', _mock_requests_match) @mock.patch('vcr.cassette.requests_match', _mock_requests_match)
def test_cassette_response_of(): def test_cassette_responses_of():
a = Cassette('test') a = Cassette('test')
a.append('foo', 'bar') 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) @mock.patch('vcr.cassette.requests_match', _mock_requests_match)
def test_cassette_get_missing_response(): def test_cassette_get_missing_response():
a = Cassette('test') a = Cassette('test')
with pytest.raises(KeyError): with pytest.raises(UnhandledHTTPRequestError):
a.response_of('foo') 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.load_cassette', lambda *args, **kwargs: (('foo',), (mock.MagicMock(),)))
@mock.patch('vcr.cassette.Cassette.can_play_response_for', return_value=True)
@mock.patch('vcr.stubs.VCRHTTPResponse')
def test_function_decorated_with_use_cassette_can_be_invoked_multiple_times(*args):
decorated_function = Cassette.use('test')(make_get_request)
for i in range(2):
decorated_function()
def test_arg_getter_functionality():
arg_getter = mock.Mock(return_value=('test', {}))
context_decorator = Cassette.use_arg_getter(arg_getter)
with context_decorator as cassette:
assert cassette._path == 'test'
arg_getter.return_value = ('other', {})
with context_decorator as cassette:
assert cassette._path == 'other'
arg_getter.return_value = ('', {'filter_headers': ('header_name',)})
@context_decorator
def function():
pass
with mock.patch.object(Cassette, 'load') as cassette_load:
function()
cassette_load.assert_called_once_with(arg_getter.return_value[0],
**arg_getter.return_value[1])
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 contextlib2.ExitStack() as exit_stack:
first_cassette = exit_stack.enter_context(Cassette.use('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('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('test'):
first_cassette_HTTPConnection = httplib.HTTPConnection
with Cassette.use('test'):
second_cassette_HTTPConnection = httplib.HTTPConnection
assert second_cassette_HTTPConnection is not first_cassette_HTTPConnection
with Cassette.use('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

View File

@@ -0,0 +1,37 @@
from vcr.filters import remove_headers, remove_query_parameters
from vcr.request import Request
def test_remove_headers():
headers = {'hello': ['goodbye'], 'secret': ['header']}
request = Request('GET', 'http://google.com', '', headers)
remove_headers(request, ['secret'])
assert request.headers == {'hello': 'goodbye'}
def test_remove_headers_empty():
headers = {'hello': 'goodbye', 'secret': 'header'}
request = Request('GET', 'http://google.com', '', headers)
remove_headers(request, [])
assert request.headers == headers
def test_remove_query_parameters():
uri = 'http://g.com/?q=cowboys&w=1'
request = Request('GET', uri, '', {})
remove_query_parameters(request, ['w'])
assert request.uri == 'http://g.com/?q=cowboys'
def test_remove_all_query_parameters():
uri = 'http://g.com/?q=cowboys&w=1'
request = Request('GET', uri, '', {})
remove_query_parameters(request, ['w', 'q'])
assert request.uri == 'http://g.com/'
def test_remove_nonexistent_query_parameters():
uri = 'http://g.com/'
request = Request('GET', uri, '', {})
remove_query_parameters(request, ['w', 'q'])
assert request.uri == 'http://g.com/'

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"

View File

@@ -0,0 +1,56 @@
import itertools
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
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
import vcr.persist
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:
vcr.persist.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:
vcr.persist.load_cassette(cassette_path, serializer)
assert "run the migration script" not in excinfo.exconly()

View File

@@ -1,11 +1,38 @@
import pytest
from vcr.request import Request from vcr.request import Request
def test_url():
req = Request('http', 'www.google.com', 80, 'GET', '/', '', {})
assert req.url == 'http://www.google.com/'
def test_str(): def test_str():
req = Request('http', 'www.google.com', 80, 'GET', '/', '', {}) req = Request('GET', 'http://www.google.com/', '', {})
str(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.add_header('X-Header1', 'h11')
assert req.headers == {'X-Header1': 'h11', 'X-Header2': 'h2'}
@pytest.mark.parametrize("uri, expected_port", [
('http://go.com/', 80),
('http://go.com:80/', 80),
('http://go.com:3000/', 3000),
('https://go.com/', 433),
('https://go.com:433/', 433),
('https://go.com: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/'

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,35 @@
import mock
import pytest
from vcr.serialize import deserialize
from vcr.serializers import yamlserializer, jsonserializer
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)
@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({})

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

@@ -0,0 +1,76 @@
import mock
import pytest
from vcr import VCR, use_cassette
from vcr.request import Request
def test_vcr_use_cassette():
record_mode = mock.Mock()
test_vcr = VCR(record_mode=record_mode)
with mock.patch('vcr.cassette.Cassette.load') as mock_cassette_load:
@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',), before_record_request=before_record_cb,
ignore_hosts=('www.test.com',), ignore_localhost=True,
filter_query_parameters=('foo',))
with test_vcr.use_cassette('test') as cassette:
assert cassette.filter_request(Request('GET', base_path + 'get', '', {})) is None
assert cassette.filter_request(Request('GET', base_path + 'get2', '', {})) is not None
assert cassette.filter_request(Request('GET', base_path + '?foo=bar', '', {})).query == []
assert cassette.filter_request(
Request('GET', base_path + '?foo=bar', '',
{'cookie': 'test', 'other': 'fun'})).headers == {'other': 'fun'}
assert cassette.filter_request(Request('GET', base_path + '?foo=bar', '',
{'cookie': 'test', 'other': 'fun'})).headers == {'other': 'fun'}
assert cassette.filter_request(Request('GET', 'http://www.test.com' + '?foo=bar', '',
{'cookie': 'test', 'other': 'fun'})) is None
with test_vcr.use_cassette('test', before_record_request=None) as cassette:
# Test that before_record can be overwritten with
assert cassette.filter_request(Request('GET', base_path + 'get', '', {})) is not None
@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

46
tox.ini
View File

@@ -1,35 +1,25 @@
# 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] [tox]
envlist = py26, py27, pypy, py26requests, py27requests, pypyrequests envlist = {py26,py27,py33,py34,pypy}-{requests25,requests24,requests23,requests22,requests1,httplib2,urllib3,boto}
[testenv] [testenv]
commands = commands =
python setup.py test py.test {posargs}
basepython =
py26: python2.6
py27: python2.7
py33: python3.3
py34: python3.4
pypy: pypy
deps = deps =
mock
pytest pytest
pytest-localserver
PyYAML PyYAML
requests1: requests==1.2.3
[testenv:py26requests] requests25: requests==2.5.0
basepython = python2.6 requests24: requests==2.4.0
deps = requests23: requests==2.3.0
pytest requests22: requests==2.2.1
PyYAML httplib2: httplib2
requests urllib3: urllib3==1.7.1
boto: boto
[testenv:py27requests]
basepython = python2.7
deps =
pytest
PyYAML
requests
[testenv:pypyrequests]
basepython = pypy
deps =
pytest
PyYAML
requests

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() default_vcr = VCR()
use_cassette = default_vcr.use_cassette
# Also, make a 'load' function available
def use_cassette(path, **kwargs):
return default_vcr.use_cassette(path, **kwargs)

View File

@@ -1,47 +1,115 @@
'''The container for recorded requests and responses''' """The container for recorded requests and responses"""
import logging
import contextlib2
import wrapt
try: try:
from collections import Counter, OrderedDict from collections import Counter
except ImportError: except ImportError:
from .compat.counter import Counter from .compat.counter import Counter
from .compat.ordereddict import OrderedDict
# Internal imports # Internal imports
from .patch import install, reset from .patch import CassettePatcherBuilder
from .persist import load_cassette, save_cassette from .persist import load_cassette, save_cassette
from .serializers import yamlserializer from .serializers import yamlserializer
from .matchers import requests_match, url, method from .matchers import requests_match, uri, method
from .errors import UnhandledHTTPRequestError
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.
"""
@classmethod
def from_args(cls, cassette_class, path, **kwargs):
return cls(cassette_class, lambda: (path, kwargs))
def __init__(self, cls, args_getter):
self.cls = cls
self._args_getter = args_getter
self.__finish = None
def _patch_generator(self, cassette):
with contextlib2.ExitStack() as exit_stack:
for patcher in CassettePatcherBuilder(cassette).build():
exit_stack.enter_context(patcher)
log.debug('Entered context for cassette at {0}.'.format(cassette._path))
yield cassette
log.debug('Exiting context for cassette at {0}.'.format(cassette._path))
# TODO(@IvanMalison): Hmmm. it kind of feels like this should be
# somewhere else.
cassette._save()
def __enter__(self):
assert self.__finish is None, "Cassette already open."
path, kwargs = self._args_getter()
self.__finish = self._patch_generator(self.cls.load(path, **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):
with self:
return function(*args, **kwargs)
class Cassette(object): class Cassette(object):
'''A container for recorded requests and responses''' """A container for recorded requests and responses"""
@classmethod @classmethod
def load(cls, path, **kwargs): def load(cls, path, **kwargs):
'''Load in the cassette stored at the provided path''' """Instantiate and load the cassette stored at the specified path."""
new_cassette = cls(path, **kwargs) new_cassette = cls(path, **kwargs)
new_cassette._load() new_cassette._load()
return new_cassette return new_cassette
def __init__(self, @classmethod
path, def use_arg_getter(cls, arg_getter):
serializer=yamlserializer, return CassetteContextDecorator(cls, arg_getter)
record_mode='once',
match_on=[url, method]): @classmethod
def use(cls, *args, **kwargs):
return CassetteContextDecorator.from_args(cls, *args, **kwargs)
def __init__(self, path, serializer=yamlserializer, record_mode='once',
match_on=(uri, method), filter_headers=(),
filter_query_parameters=(), before_record_request=None,
before_record_response=None, ignore_hosts=(),
ignore_localhost=()):
self._path = path self._path = path
self._serializer = serializer self._serializer = serializer
self._match_on = match_on 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.data is the list of (req, resp) tuples # self.data is the list of (req, resp) tuples
self.data = [] self.data = []
self.play_counts = Counter() self.play_counts = Counter()
self.dirty = False self.dirty = False
self.rewound = False
self.record_mode = record_mode self.record_mode = record_mode
@property @property
def play_count(self): def play_count(self):
return sum(self.play_counts.values()) 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 @property
def requests(self): def requests(self):
return [request for (request, response) in self.data] return [request for (request, response) in self.data]
@@ -50,47 +118,70 @@ class Cassette(object):
def responses(self): def responses(self):
return [response for (request, response) in self.data] 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 @property
def write_protected(self): def write_protected(self):
return self.rewound and self.record_mode == 'once' or \ return self.rewound and self.record_mode == 'once' or \
self.record_mode == 'none' 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): 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
if self._before_record_response:
response = self._before_record_response(response)
self.data.append((request, response)) self.data.append((request, response))
self.dirty = True self.dirty = True
def response_of(self, request): def filter_request(self, request):
''' return self._before_record_request(request)
Find the response corresponding to a request
''' def _responses(self, request):
responses = [] """
for stored_request, response in self.data: 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): if requests_match(request, stored_request, self._match_on):
responses.append(response) yield index, response
index = self.play_counts[request]
try: def can_play_response_for(self, request):
return responses[index] request = self._before_record_request(request)
except IndexError: return request and request in self and \
# I decided that a KeyError is the best exception to raise self.record_mode != 'all' and \
# if the cassette doesn't contain the request asked for. self.rewound
raise KeyError
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): def _as_dict(self):
return {"requests": self.requests, "responses": self.responses} return {"requests": self.requests, "responses": self.responses}
@@ -113,6 +204,7 @@ class Cassette(object):
for request, response in zip(requests, responses): for request, response in zip(requests, responses):
self.append(request, response) self.append(request, response)
self.dirty = False self.dirty = False
self.rewound = True
except IOError: except IOError:
pass pass
@@ -122,21 +214,12 @@ class Cassette(object):
) )
def __len__(self): 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) return len(self.data)
def __contains__(self, request): def __contains__(self, request):
'''Return whether or not a request has been stored''' """Return whether or not a request has been stored"""
for stored_request, response in self.data: for index, response in self._responses(request):
if requests_match(stored_request, request, self._match_on): if self.play_counts[index] == 0:
return True return True
return False 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()

View File

@@ -1,3 +1,4 @@
from __future__ import print_function
from operator import itemgetter from operator import itemgetter
from heapq import nlargest from heapq import nlargest
from itertools import repeat, ifilter from itertools import repeat, ifilter
@@ -25,7 +26,7 @@ class Counter(dict):
>>> c = Counter({'a': 4, 'b': 2}) # a new counter from a mapping >>> c = Counter({'a': 4, 'b': 2}) # a new counter from a mapping
>>> c = Counter(a=4, b=2) # a new counter from keyword args >>> c = Counter(a=4, b=2) # a new counter from keyword args
''' '''
self.update(iterable, **kwds) self.update(iterable, **kwds)
def __missing__(self, key): def __missing__(self, key):
@@ -38,7 +39,7 @@ class Counter(dict):
>>> Counter('abracadabra').most_common(3) >>> Counter('abracadabra').most_common(3)
[('a', 5), ('r', 2), ('b', 2)] [('a', 5), ('r', 2), ('b', 2)]
''' '''
if n is None: if n is None:
return sorted(self.iteritems(), key=itemgetter(1), reverse=True) return sorted(self.iteritems(), key=itemgetter(1), reverse=True)
return nlargest(n, self.iteritems(), key=itemgetter(1)) return nlargest(n, self.iteritems(), key=itemgetter(1))
@@ -77,7 +78,7 @@ class Counter(dict):
>>> c['h'] # four 'h' in which, witch, and watch >>> c['h'] # four 'h' in which, witch, and watch
4 4
''' '''
if iterable is not None: if iterable is not None:
if hasattr(iterable, 'iteritems'): if hasattr(iterable, 'iteritems'):
if self: if self:
@@ -189,5 +190,5 @@ class Counter(dict):
if __name__ == '__main__': if __name__ == '__main__':
import doctest import doctest
print doctest.testmod() print(doctest.testmod())

View File

@@ -1,16 +1,22 @@
import collections
import copy
import functools
import os import os
from .cassette import Cassette from .cassette import Cassette
from .serializers import yamlserializer, jsonserializer from .serializers import yamlserializer, jsonserializer
from .matchers import method, url, host, path, headers, body from . import matchers
from . import filters
class VCR(object): class VCR(object):
def __init__(self,
serializer='yaml', def __init__(self, serializer='yaml', cassette_library_dir=None,
cassette_library_dir=None, record_mode="once", filter_headers=(),
record_mode="once", filter_query_parameters=(), before_record_request=None,
match_on=['url', 'method'], before_record_response=None, ignore_hosts=(),
): match_on=('method', 'scheme', 'host', 'port', 'path', 'query',),
ignore_localhost=False, before_record=None):
self.serializer = serializer self.serializer = serializer
self.match_on = match_on self.match_on = match_on
self.cassette_library_dir = cassette_library_dir self.cassette_library_dir = cassette_library_dir
@@ -19,52 +25,143 @@ class VCR(object):
'json': jsonserializer, 'json': jsonserializer,
} }
self.matchers = { self.matchers = {
'method': method, 'method': matchers.method,
'url': url, 'uri': matchers.uri,
'host': host, 'url': matchers.uri, # matcher for backwards compatibility
'path': path, 'scheme': matchers.scheme,
'headers': headers, 'host': matchers.host,
'body': body, 'port': matchers.port,
'path': matchers.path,
'query': matchers.query,
'headers': matchers.headers,
'body': matchers.body,
} }
self.record_mode = record_mode self.record_mode = record_mode
self.filter_headers = filter_headers
self.filter_query_parameters = filter_query_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
def _get_serializer(self, serializer_name): def _get_serializer(self, serializer_name):
try: try:
serializer = self.serializers[serializer_name] serializer = self.serializers[serializer_name]
except KeyError: except KeyError:
print "Serializer {0} doesn't exist or isn't registered".format( print("Serializer {0} doesn't exist or isn't registered".format(
serializer_name serializer_name
) ))
raise KeyError raise KeyError
return serializer return serializer
def _get_matchers(self, matcher_names): def _get_matchers(self, matcher_names):
matchers = []
try: try:
matchers = [self.matchers[m] for m in matcher_names] for m in matcher_names:
matchers.append(self.matchers[m])
except KeyError: except KeyError:
raise KeyError( raise KeyError(
"Matcher {0} doesn't exist or isn't registered".format(m) "Matcher {0} doesn't exist or isn't registered".format(m)
) )
return matchers return matchers
def use_cassette(self, path, **kwargs): def use_cassette(self, path, with_current_defaults=False, **kwargs):
if with_current_defaults:
return Cassette.use(path, self.get_path_and_merged_config(path, **kwargs))
args_getter = functools.partial(self.get_path_and_merged_config, path, **kwargs)
return Cassette.use_arg_getter(args_getter)
def get_path_and_merged_config(self, path, **kwargs):
serializer_name = kwargs.get('serializer', self.serializer) serializer_name = kwargs.get('serializer', self.serializer)
matcher_names = kwargs.get('match_on', self.match_on) matcher_names = kwargs.get('match_on', self.match_on)
cassette_library_dir = kwargs.get( cassette_library_dir = kwargs.get(
'cassette_library_dir', 'cassette_library_dir',
self.cassette_library_dir self.cassette_library_dir
) )
if cassette_library_dir: if cassette_library_dir:
path = os.path.join(cassette_library_dir, path) path = os.path.join(cassette_library_dir, path)
merged_config = { merged_config = {
"serializer": self._get_serializer(serializer_name), 'serializer': self._get_serializer(serializer_name),
"match_on": self._get_matchers(matcher_names), 'match_on': self._get_matchers(matcher_names),
"record_mode": kwargs.get('record_mode', self.record_mode), '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)
} }
return path, 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
)
filter_functions = []
if before_record_response and not isinstance(before_record_response,
collections.Iterable):
before_record_response = (before_record_response,)
for function in before_record_response:
filter_functions.append(function)
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
)
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:
filter_functions.append(functools.partial(filters.remove_headers,
headers_to_remove=filter_headers))
if filter_query_parameters:
filter_functions.append(functools.partial(filters.remove_query_parameters,
query_parameters_to_remove=filter_query_parameters))
hosts_to_ignore = list(ignore_hosts)
if ignore_localhost:
hosts_to_ignore.extend(('localhost', '0.0.0.0', '127.0.0.1'))
if hosts_to_ignore:
hosts_to_ignore = set(hosts_to_ignore)
filter_functions.append(self._build_ignore_hosts(hosts_to_ignore))
if before_record_request:
if not isinstance(before_record_request, collections.Iterable):
before_record_request = (before_record_request,)
for function in before_record_request:
filter_functions.append(function)
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
def register_serializer(self, name, serializer): def register_serializer(self, name, serializer):
self.serializers[name] = serializer self.serializers[name] = serializer

10
vcr/errors.py Normal file
View File

@@ -0,0 +1,10 @@
class CannotOverwriteExistingCassetteException(Exception):
pass
class UnhandledHTTPRequestError(KeyError):
'''
Raised when a cassette does not c
ontain the request we want
'''
pass

24
vcr/filters.py Normal file
View File

@@ -0,0 +1,24 @@
from six.moves.urllib.parse import urlparse, urlencode, urlunparse
import copy
def remove_headers(request, headers_to_remove):
headers = copy.copy(request.headers)
headers_to_remove = [h.lower() for h in headers_to_remove]
keys = [k for k in headers if k.lower() in headers_to_remove]
if keys:
for k in keys:
headers.pop(k)
request.headers = headers
return request
def remove_query_parameters(request, query_parameters_to_remove):
query = request.query
new_query = [(k, v) for (k, v) in query
if k not in query_parameters_to_remove]
if len(new_query) != len(query):
uri_parts = list(urlparse(request.uri))
uri_parts[4] = urlencode(new_query)
request.uri = urlunparse(uri_parts)
return request

View File

@@ -1,19 +1,35 @@
import logging
log = logging.getLogger(__name__)
def method(r1, r2): def method(r1, r2):
return r1.method == r2.method return r1.method == r2.method
def url(r1, r2): def uri(r1, r2):
return r1.url == r2.url return r1.uri == r2.uri
def host(r1, r2): def host(r1, r2):
return r1.host == r2.host 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): def path(r1, r2):
return r1.path == r2.path return r1.path == r2.path
def query(r1, r2):
return r1.query == r2.query
def body(r1, r2): def body(r1, r2):
return r1.body == r2.body return r1.body == r2.body
@@ -22,5 +38,16 @@ def headers(r1, r2):
return r1.headers == r2.headers 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): 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])

168
vcr/migration.py Normal file
View File

@@ -0,0 +1,168 @@
"""
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 compat, 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': 433, '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,306 @@
'''Utilities for patching in cassettes''' '''Utilities for patching in cassettes'''
import functools
import itertools
import contextlib2
import mock
import httplib
from .stubs import VCRHTTPConnection, VCRHTTPSConnection from .stubs import VCRHTTPConnection, VCRHTTPSConnection
from six.moves import http_client as httplib
# Save some of the original types for the purposes of unpatching # Save some of the original types for the purposes of unpatching
_HTTPConnection = httplib.HTTPConnection _HTTPConnection = httplib.HTTPConnection
_HTTPSConnection = httplib.HTTPSConnection _HTTPSConnection = httplib.HTTPSConnection
# Try to save the original types for requests
try: try:
# Try to save the original types for requests
import requests.packages.urllib3.connectionpool as cpool import requests.packages.urllib3.connectionpool as cpool
except ImportError: # pragma: no cover
pass
else:
_VerifiedHTTPSConnection = cpool.VerifiedHTTPSConnection _VerifiedHTTPSConnection = cpool.VerifiedHTTPSConnection
_HTTPConnection = cpool.HTTPConnection _cpoolHTTPConnection = cpool.HTTPConnection
except ImportError: # pragma: no cover _cpoolHTTPSConnection = cpool.HTTPSConnection
pass
# Try to save the original types for urllib3
try: try:
# Try to save the original types for urllib3
import urllib3 import urllib3
_VerifiedHTTPSConnection = urllib3.connectionpool.VerifiedHTTPSConnection
except ImportError: # pragma: no cover except ImportError: # pragma: no cover
pass pass
else:
_VerifiedHTTPSConnection = urllib3.connectionpool.VerifiedHTTPSConnection
def install(cassette): # Try to save the original types for httplib2
""" try:
Patch all the HTTPConnections references we can find! import httplib2
This replaces the actual HTTPConnection with a VCRHTTPConnection except ImportError: # pragma: no cover
object which knows how to save to / read from cassettes pass
""" else:
httplib.HTTPConnection = httplib.HTTP._connection_class = VCRHTTPConnection _HTTPConnectionWithTimeout = httplib2.HTTPConnectionWithTimeout
httplib.HTTPSConnection = httplib.HTTPS._connection_class = ( _HTTPSConnectionWithTimeout = httplib2.HTTPSConnectionWithTimeout
VCRHTTPSConnection) _SCHEME_TO_CONNECTION = httplib2.SCHEME_TO_CONNECTION
httplib.HTTPConnection.cassette = cassette
httplib.HTTPSConnection.cassette = cassette
# patch requests
# 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
class CassettePatcherBuilder(object):
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._urllib3(), self._httplib2(),
self._boto())
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):
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.requests_stubs import VCRRequestsHTTPConnection, VCRRequestsHTTPSConnection
http_connection_remover = ConnectionRemover(
self._get_cassette_subclass(VCRRequestsHTTPConnection)
)
https_connection_remover = ConnectionRemover(
self._get_cassette_subclass(VCRRequestsHTTPSConnection)
)
mock_triples = (
(cpool, 'VerifiedHTTPSConnection', VCRRequestsHTTPSConnection),
(cpool, 'VerifiedHTTPSConnection', VCRRequestsHTTPSConnection),
(cpool, 'HTTPConnection', VCRRequestsHTTPConnection),
(cpool, 'HTTPSConnection', VCRRequestsHTTPSConnection),
(cpool, 'is_connection_dropped', mock.Mock(return_value=False)), # Needed on Windows only
(cpool.HTTPConnectionPool, 'ConnectionCls', VCRRequestsHTTPConnection),
(cpool.HTTPSConnectionPool, 'ConnectionCls', VCRRequestsHTTPSConnection),
)
# These handle making sure that sessions only use the
# connections of the appropriate type.
mock_triples += ((cpool.HTTPConnectionPool, '_get_conn',
self._patched_get_conn(cpool.HTTPConnectionPool,
lambda : cpool.HTTPConnection)),
(cpool.HTTPSConnectionPool, '_get_conn',
self._patched_get_conn(cpool.HTTPSConnectionPool,
lambda : cpool.HTTPSConnection)),
(cpool.HTTPConnectionPool, '_new_conn',
self._patched_new_conn(cpool.HTTPConnectionPool,
http_connection_remover)),
(cpool.HTTPSConnectionPool, '_new_conn',
self._patched_new_conn(cpool.HTTPSConnectionPool,
https_connection_remover)))
return itertools.chain(self._build_patchers_from_mock_triples(mock_triples),
(http_connection_remover, https_connection_remover))
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()
while not isinstance(connection, connection_class):
connection = get_conn(pool, timeout)
return connection
return patched_get_conn
def _patched_new_conn(self, connection_pool_class, connection_remover):
new_conn = connection_pool_class._new_conn
@functools.wraps(new_conn)
def patched_new_conn(pool):
new_connection = new_conn(pool)
connection_remover.add_connection_to_pool_entry(pool, new_connection)
return new_connection
return patched_new_conn
@_build_patchers_from_mock_triples_decorator
def _urllib3(self):
try:
import urllib3.connectionpool as cpool
except ImportError: # pragma: no cover
pass
else:
from .stubs.urllib3_stubs import VCRVerifiedHTTPSConnection
yield cpool, 'VerifiedHTTPSConnection', VCRVerifiedHTTPSConnection
yield cpool, 'HTTPConnection', VCRHTTPConnection
@_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
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: try:
import requests.packages.urllib3.connectionpool as cpool 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 except ImportError: # pragma: no cover
pass 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 if hasattr(cpool, 'HTTPSConnection'):
try: yield mock.patch.object(cpool, 'HTTPSConnection', _cpoolHTTPSConnection)
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
try: try:
import urllib3.connectionpool as cpool import urllib3.connectionpool as cpool
cpool.VerifiedHTTPSConnection = _VerifiedHTTPSConnection
cpool.HTTPConnection = _HTTPConnection
except ImportError: # pragma: no cover except ImportError: # pragma: no cover
pass 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 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)
@contextlib2.contextmanager
def force_reset():
with contextlib2.ExitStack() as exit_stack:
for patcher in reset_patchers():
exit_stack.enter_context(patcher)
yield

View File

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

View File

@@ -1,23 +1,11 @@
import tempfile
import os import os
class FilesystemPersister(object): 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 @classmethod
def write(cls, cassette_path, data): def write(cls, cassette_path, data):
dirname, filename = os.path.split(cassette_path) dirname, filename = os.path.split(cassette_path)
if dirname and not os.path.exists(dirname): if dirname and not os.path.exists(dirname):
os.makedirs(dirname) os.makedirs(dirname)
cls._secure_write(cassette_path, data) with open(cassette_path, 'w') as f:
f.write(data)

View File

@@ -1,50 +1,90 @@
class Request(object): from six.moves.urllib.parse import urlparse, parse_qsl
def __init__(self, protocol, host, port, method, path, body, headers):
self.protocol = protocol class Request(object):
self.host = host """
self.port = port VCR's representation of a request.
There is a weird quirk in HTTP. You can send the same header twice. For
this reason, headers are represented by a dict, with lists as the values.
However, it appears that HTTPlib is completely incapable of sending the
same header twice. This puts me in a weird position: I want to be able to
accurately represent HTTP headers in cassettes, but I don't want the extra
step of always having to do [0] in the general case, i.e.
request.headers['key'][0]
In addition, some servers sometimes send the same header more than once,
and httplib *can* deal with this situation.
Futhermore, I wanted to keep the request and response cassette format as
similar as possible.
For this reason, in cassettes I keep a dict with lists as keys, but once
deserialized into VCR, I keep them as plain, naked dicts.
"""
def __init__(self, method, uri, body, headers):
self.method = method self.method = method
self.path = path self.uri = uri
self.body = body self.body = body
# make headers a frozenset so it will be hashable self.headers = {}
self.headers = frozenset(headers.items()) for key in headers:
self.add_header(key, headers[key])
def add_header(self, key, value):
# see class docstring for an explanation
if isinstance(value, (tuple, list)):
self.headers[key] = value[0]
else:
self.headers[key] = value
@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': 433, '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 @property
def url(self): def url(self):
return "{0}://{1}{2}".format(self.protocol, self.host, self.path) return self.uri
def __key(self): # alias for backwards compatibility
return ( @property
self.host, def protocol(self):
self.port, return self.scheme
self.method,
self.path,
self.body,
self.headers
)
def __hash__(self):
return hash(self.__key())
def __eq__(self, other):
return hash(self) == hash(other)
def __str__(self): def __str__(self):
return "<Request ({0}) {1}>".format(self.method, self.url) return "<Request ({0}) {1}>".format(self.method, self.uri)
def __repr__(self): def __repr__(self):
return self.__str__() return self.__str__()
def _to_dict(self): def _to_dict(self):
return { return {
'protocol': self.protocol,
'host': self.host,
'port': self.port,
'method': self.method, 'method': self.method,
'path': self.path, 'uri': self.uri,
'body': self.body, 'body': self.body,
'headers': self.headers, 'headers': dict(((k, [v]) for k, v in self.headers.items())),
} }
@classmethod @classmethod

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': 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)

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

@@ -0,0 +1,55 @@
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 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_body_to_unicode(resp):
"""
If the request body is bytes, decode it to a string (for python3 support)
"""
try:
if not isinstance(resp['body']['string'], six.text_type):
resp['body']['string'] = resp['body']['string'].decode('utf-8')
except (KeyError, TypeError, UnicodeDecodeError):
# The thing we were converting either wasn't a dictionary or didn't
# have the keys we were expecting. Some of the tests just serialize
# and deserialize a string.
# Also, sometimes the thing actually is binary, so if you can't decode
# it, just give up.
pass
return resp

View File

@@ -1,34 +1,24 @@
from vcr.request import Request
try: try:
import simplejson as json import simplejson as json
except ImportError: except ImportError:
import json 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): def deserialize(cassette_string):
data = json.loads(cassette_string) return 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
def serialize(cassette_dict): def serialize(cassette_dict):
data = ([{ try:
'request': request._to_dict(), return json.dumps(cassette_dict, indent=4)
'response': response, except UnicodeDecodeError as original:
} for request, response in zip( raise UnicodeDecodeError(
cassette_dict['requests'], original.encoding,
cassette_dict['responses'] b"Error serializing cassette to JSON",
)]) original.start,
return json.dumps(data, indent=4, default=_json_default) original.end,
original.args[-1] +
("Does this HTTP interaction contain binary data? "
"If so, use a different serializer (like the yaml serializer) "
"for this request?")
)

View File

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

View File

@@ -1,74 +1,188 @@
'''Stubs for patching HTTP and HTTPS requests''' '''Stubs for patching HTTP and HTTPS requests'''
from httplib import HTTPConnection, HTTPSConnection, HTTPMessage try:
from cStringIO import StringIO import http.client
except ImportError:
pass
import logging
import six
from six.moves.http_client import (
HTTPConnection,
HTTPSConnection,
HTTPMessage,
HTTPResponse,
)
from six import BytesIO
from vcr.request import Request 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 Stub reponse class that gets returned instead of a HTTPResponse
""" """
def __init__(self, recorded_response): def __init__(self, recorded_response):
self.recorded_response = recorded_response self.recorded_response = recorded_response
self.reason = recorded_response['status']['message'] self.reason = recorded_response['status']['message']
self.status = recorded_response['status']['code'] self.status = self.code = recorded_response['status']['code']
self.version = None 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 headers = self.recorded_response['headers']
# at this point) and directly adding the headers to the header self.headers = self.msg = parse_headers(headers)
# container, so just pass an empty StringIO.
self.msg = HTTPMessage(StringIO(''))
for key, val in self.recorded_response['headers'].iteritems(): self.length = compat.get_header(self.msg, 'content-length') or None
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 = 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): 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) return self._content.read(*args, **kwargs)
def readline(self, *args, **kwargs):
return self._content.readline(*args, **kwargs)
def close(self): def close(self):
self._closed = True
return True return True
def getcode(self):
return self.status
def isclosed(self): def isclosed(self):
# Urllib3 seems to call this because it actually uses return self.closed
# the weird chunking support in httplib
return True def info(self):
return parse_headers(self.recorded_response['headers'])
def getheaders(self): 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 # A reference to the cassette that's currently being patched in
cassette = None cassette = None
def _port_postfix(self):
"""
Returns empty string for the default port and ':port' otherwise
"""
port = self.real_connection.port
default_port = {'https': 433, 'http': 80}[self._protocol]
return ':{0}'.format(port) if port != default_port else ''
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): def request(self, method, url, body=None, headers=None):
'''Persist the request metadata in self._vcr_request''' '''Persist the request metadata in self._vcr_request'''
self._vcr_request = Request( self._vcr_request = Request(
protocol=self._protocol,
host=self.host,
port=self.port,
method=method, method=method,
path=url, uri=self._uri(url),
body=body, body=body,
headers=headers or {} headers=headers or {}
) )
log.debug('Got {0}'.format(self._vcr_request))
# Note: The request may not actually be finished at this point, so # Note: The request may not actually be finished at this point, so
# I'm not sending the actual request until getresponse(). This # I'm not sending the actual request until getresponse(). This
# allows me to compare the entire length of the response to see if it # allows me to compare the entire length of the response to see if it
# exists in the cassette. # 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):
for value in values:
self._vcr_request.add_header(header, value)
def send(self, data): def send(self, data):
''' '''
This method is called after request(), to add additional data to the This method is called after request(), to add additional data to the
@@ -77,96 +191,60 @@ class VCRConnectionMixin:
''' '''
self._vcr_request.body = (self._vcr_request.body or '') + data self._vcr_request.body = (self._vcr_request.body or '') + 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, *args, **kwargs):
""" """
Copy+pasted from python stdlib 2.6 source because it Normally, this would atually send the request to the server.
has a call to self.send() which I have overridden We are not sending the request until getting the response,
#stdlibproblems #fml so bypass this method for now.
""" """
header_names = dict.fromkeys([k.lower() for k in headers]) pass
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)
def getresponse(self, _=False): def getresponse(self, _=False):
'''Retrieve a the response''' '''Retrieve the response'''
# Check to see if the cassette has a response for this request. If so, # Check to see if the cassette has a response for this request. If so,
# then return it # then return it
if self._vcr_request in self.cassette and \ if self.cassette.can_play_response_for(self._vcr_request):
self.cassette.record_mode != "all": log.info(
response = self.cassette.response_of(self._vcr_request) "Playing response for {0} from cassette".format(
# Alert the cassette to the fact that we've served another self._vcr_request
# response for the provided requests )
self.cassette.mark_played(self._vcr_request) )
response = self.cassette.play_response(self._vcr_request)
return VCRHTTPResponse(response) return VCRHTTPResponse(response)
else: else:
if self.cassette.write_protected: if self.cassette.write_protected and self.cassette.filter_request(
raise Exception("cassette is write protected") 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 # Otherwise, we should send the request, then get the response
# and return it. # and return it.
# make the request log.info(
self._baseclass.request( "{0} not in cassette, sending to real server".format(
self, self._vcr_request
)
)
self.real_connection.request(
method=self._vcr_request.method, method=self._vcr_request.method,
url=self._vcr_request.path, url=self._url(self._vcr_request.uri),
body=self._vcr_request.body, body=self._vcr_request.body,
headers=dict(self._vcr_request.headers or {}) headers=self._vcr_request.headers,
) )
# get the response # get the response
response = self._baseclass.getresponse(self) response = self.real_connection.getresponse()
# put the response into the cassette # put the response into the cassette
response = { response = {
@@ -174,32 +252,66 @@ class VCRConnectionMixin:
'code': response.status, 'code': response.status,
'message': response.reason 'message': response.reason
}, },
'headers': dict(response.getheaders()), 'headers': serialize_headers(response),
'body': {'string': response.read()}, 'body': {'string': response.read()},
} }
self.cassette.append(self._vcr_request, response) self.cassette.append(self._vcr_request, response)
return VCRHTTPResponse(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
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)
class VCRHTTPConnection(VCRConnection):
'''A Mocked class for HTTP requests''' '''A Mocked class for HTTP requests'''
# Can't use super since this is an old-style class
_baseclass = HTTPConnection _baseclass = HTTPConnection
_protocol = 'http' _protocol = 'http'
def __init__(self, *args, **kwargs):
HTTPConnection.__init__(self, *args, **kwargs)
class VCRHTTPSConnection(VCRConnection):
class VCRHTTPSConnection(VCRConnectionMixin, HTTPSConnection):
'''A Mocked class for HTTPS requests''' '''A Mocked class for HTTPS requests'''
_baseclass = HTTPSConnection _baseclass = HTTPSConnection
_protocol = 'https' _protocol = 'https'
is_verified = True
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)

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,62 @@
'''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',
))
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,13 @@
'''Stubs for requests''' '''Stubs for requests'''
from requests.packages.urllib3.connectionpool import VerifiedHTTPSConnection from requests.packages.urllib3.connectionpool import HTTPConnection, VerifiedHTTPSConnection
from ..stubs import VCRHTTPSConnection 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 _baseclass = VerifiedHTTPSConnection