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

Compare commits

...

244 Commits

Author SHA1 Message Date
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
Kevin McCarthy
2275749eaa bump for version 0.3.4 2013-10-24 19:56:36 -10:00
smallcode
16fbe40d87 Update filesystem.py
fix WindowsError: [Error 32].
because must close the file before rename the file in window system.
2013-10-22 17:41:56 +08:00
Kevin McCarthy
deed8cab97 Fix issue #36 - error message for unregistered matcher was broken 2013-09-29 15:56:50 -10:00
Kevin McCarthy
cf8646d8d6 Bump version for bugfix release 2013-09-21 16:52:09 -10:00
Hector Dearman
c03459e582 allow match_on to be passed as an argument VCR 2013-09-21 16:52:09 -10:00
Kevin McCarthy
912452e863 Only use the relative path in HTTP requests
This causes a pretty big problem on out-of-spec HTTP servers (like
Flickr). Closes #31
2013-09-17 13:19:26 -10:00
Kevin McCarthy
ce3d7270ea Update README.md 2013-09-16 20:56:16 -10:00
Kevin McCarthy
39d696bc49 Update README.md 2013-09-16 20:54:27 -10:00
Kevin McCarthy
ce94fd72fd bump development status to Beta 2013-09-16 20:52:38 -10:00
Kevin McCarthy
a66f462dcd Add support for custom request matchers
This commit not only changes the default method of matching requests
(just match on method and URI instead of the entire request + headers)
but also allows the user to add custom matchers.
2013-09-16 20:46:00 -10:00
Kevin McCarthy
03c22d79dd Add support for configurable record modes
This support will let you select one of four different behaviors
for VCR's cassettes.  Closes #23
2013-09-15 18:43:02 -10:00
Kevin McCarthy
5ce67dc023 Add support for calling httplib.send().
This commit changes the whole core internal flow of requests.
Now, requests are actually physically made lazily when a response
is requested.  This allows the entire request to be sent at once.

Otherwise, it would be impossible to compare whether requests have
already been recorded, since httplib.send() allows you to effectively
stream requests over HTTP.
2013-09-15 18:43:02 -10:00
60 changed files with 3623 additions and 659 deletions

2
.gitignore vendored
View File

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

View File

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

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:

530
README.md
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,131 +1,146 @@
- request: !!python/object:vcr.request.Request
version: 1
interactions:
- request:
body: null
headers: !!python/object/apply:__builtin__.frozenset
- - !!python/tuple [Accept-Encoding, 'gzip, deflate, compress']
- !!python/tuple [User-Agent, vcrpy-test]
- !!python/tuple [Accept, '*/*']
host: moz.com
headers:
Accept: ['*/*']
Accept-Encoding: ['gzip, deflate, compress']
User-Agent: ['vcrpy-test']
method: GET
path: /
port: 80
protocol: http
uri: http://seomoz.org/
response:
body: {string: ''}
headers:
Location: ['http://moz.com/']
Server: ['BigIP']
Connection: ['Keep-Alive']
Content-Length: ['0']
status: {code: 301, message: Moved Permanently}
- request:
body: null
headers:
Accept: ['*/*']
Accept-Encoding: ['gzip, deflate, compress']
User-Agent: ['vcrpy-test']
method: GET
uri: http://moz.com/
response:
body:
string: !!binary |
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
headers: {accept-ranges: bytes, age: '3593', cache-control: 'no-cache, must-revalidate,
s-maxage=3600', connection: keep-alive, content-encoding: gzip, content-length: '5300',
content-type: text/html, date: 'Sat, 10 Aug 2013 23:33:35 GMT', expires: 'Fri,
15 Oct 2004 12:00:00 GMT', server: nginx, server-name: dalmozwww02.dal.moz.com,
vary: Accept-Encoding, via: 1.1 varnish, x-varnish: 2027569899 2027508286}
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
headers:
Server: [nginx]
Content-Type: [text/html]
Vary: [Accept-Encoding]
Cache-Control: [no-cache]
must-revalidate: ['s-maxage=3600']
Expires: ['Fri 15 Oct 2004 12:00:00 GMT']
Server-Name: ['dalmozwww01.dal.moz.com']
Content-Encoding: [gzip]
Content-Length: ['5683']
Accept-Ranges: [bytes]
Date: ['Sat, 11 Jan 2014 18:45:11 GMT']
X-Varnish: [918768771 918700396]
Age: ['3479']
Via: ['1.1 varnish']
Connection: [keep-alive]
status: {code: 200, message: OK}
- request: !!python/object:vcr.request.Request
body: null
headers: !!python/object/apply:__builtin__.frozenset
- - !!python/tuple [Accept-Encoding, 'gzip, deflate, compress']
- !!python/tuple [User-Agent, vcrpy-test]
- !!python/tuple [Accept, '*/*']
host: seomoz.org
method: GET
path: /
port: 80
protocol: http
response:
body: {string: "<html>\r\n<head><title>301 Moved Permanently</title></head>\r\n<body
bgcolor=\"white\">\r\n<center><h1>301 Moved Permanently</h1></center>\r\n<hr><center>nginx</center>\r\n</body>\r\n</html>\r\n"}
headers: {accept-ranges: bytes, age: '0', connection: keep-alive, content-length: '178',
content-type: text/html, date: 'Sat, 10 Aug 2013 23:33:35 GMT', location: 'http://moz.com/',
server: nginx, server-name: dalmozwww01.dal.moz.com, via: 1.1 varnish, x-varnish: '2027569892'}
status: {code: 301, message: Moved Permanently}

View File

@@ -1,9 +1,9 @@
'''Basic tests about cassettes'''
'''Basic tests for cassettes'''
# coding=utf-8
# External imports
import os
import urllib2
from six.moves.urllib.request import urlopen
# Internal imports
import vcr
@@ -16,7 +16,7 @@ def test_nonexistent_directory(tmpdir):
# Run VCR to create dir and cassette file
with vcr.use_cassette(str(tmpdir.join('nonexistent', 'cassette.yml'))):
urllib2.urlopen('http://httpbin.org/').read()
urlopen('http://httpbin.org/').read()
# This should have made the file and the directory
assert os.path.exists(str(tmpdir.join('nonexistent', 'cassette.yml')))
@@ -25,11 +25,11 @@ def test_nonexistent_directory(tmpdir):
def test_unpatch(tmpdir):
'''Ensure that our cassette gets unpatched when we're done'''
with vcr.use_cassette(str(tmpdir.join('unpatch.yaml'))) as cass:
urllib2.urlopen('http://httpbin.org/').read()
urlopen('http://httpbin.org/').read()
# Make the same request, and assert that we haven't served any more
# requests out of cache
urllib2.urlopen('http://httpbin.org/').read()
urlopen('http://httpbin.org/').read()
assert cass.play_count == 0
@@ -38,10 +38,10 @@ def test_basic_use(tmpdir):
Copied from the docs
'''
with vcr.use_cassette('fixtures/vcr_cassettes/synopsis.yaml'):
response = urllib2.urlopen(
response = urlopen(
'http://www.iana.org/domains/reserved'
).read()
assert 'Example domains' in response
assert b'Example domains' in response
def test_basic_json_use(tmpdir):
@@ -50,8 +50,8 @@ def test_basic_json_use(tmpdir):
'''
test_fixture = 'fixtures/vcr_cassettes/synopsis.json'
with vcr.use_cassette(test_fixture, serializer='json'):
response = urllib2.urlopen('http://httpbin.org/').read()
assert 'difficult sometimes' in response
response = urlopen('http://httpbin.org/').read()
assert b'difficult sometimes' in response
def test_patched_content(tmpdir):
@@ -60,16 +60,16 @@ def test_patched_content(tmpdir):
request
'''
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
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
cass._save(force=True)
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 response == response2
@@ -85,16 +85,16 @@ def test_patched_content_json(tmpdir):
testfile = str(tmpdir.join('synopsis.json'))
with vcr.use_cassette(testfile) as cass:
response = urllib2.urlopen('http://httpbin.org/').read()
response = urlopen('http://httpbin.org/').read()
assert cass.play_count == 0
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
cass._save(force=True)
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 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 CertValidatingHTTPSConnection is 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,7 +1,8 @@
import os
import json
import urllib2
import pytest
import vcr
from six.moves.urllib.request import urlopen
def test_set_serializer_default_config(tmpdir):
@@ -9,7 +10,7 @@ def test_set_serializer_default_config(tmpdir):
with my_vcr.use_cassette(str(tmpdir.join('test.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:
assert json.loads(f.read())
@@ -19,7 +20,7 @@ def test_default_set_cassette_library_dir(tmpdir):
my_vcr = vcr.VCR(cassette_library_dir=str(tmpdir.join('subdir')))
with my_vcr.use_cassette('test.json'):
urllib2.urlopen('http://httpbin.org/get')
urlopen('http://httpbin.org/get')
assert os.path.exists(str(tmpdir.join('subdir').join('test.json')))
@@ -30,7 +31,28 @@ def test_override_set_cassette_library_dir(tmpdir):
cld = str(tmpdir.join('subdir2'))
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 not os.path.exists(str(tmpdir.join('subdir').join('test.json')))
def test_override_match_on(tmpdir):
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:
urlopen('http://httpbin.org/get')
assert len(cass) == 1
assert cass.play_count == 1
def test_missing_matcher():
my_vcr = vcr.VCR()
my_vcr.register_matcher("awesome", object)
with pytest.raises(KeyError):
with my_vcr.use_cassette("test.yaml", match_on=['notawesome']):
pass

View File

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

@@ -0,0 +1,118 @@
import os
import pytest
import vcr
from six.moves.urllib.request import urlopen
def test_once_record_mode(tmpdir):
testfile = str(tmpdir.join('recordmode.yml'))
with vcr.use_cassette(testfile, record_mode="once"):
# cassette file doesn't exist, so create.
response = urlopen('http://httpbin.org/').read()
with vcr.use_cassette(testfile, record_mode="once") as cass:
# make the same request again
response = urlopen('http://httpbin.org/').read()
# the first time, it's played from the cassette.
# but, try to access something else from the same cassette, and an
# exception is raised.
with pytest.raises(Exception):
response = urlopen('http://httpbin.org/get').read()
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):
testfile = str(tmpdir.join('recordmode.yml'))
with vcr.use_cassette(testfile, record_mode="new_episodes"):
# cassette file doesn't exist, so create.
response = urlopen('http://httpbin.org/').read()
with vcr.use_cassette(testfile, record_mode="new_episodes") as cass:
# make the same request again
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
# a cassette without repurcussions.
response = urlopen('http://httpbin.org/get').read()
# one of the responses has been played
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_all_record_mode(tmpdir):
testfile = str(tmpdir.join('recordmode.yml'))
with vcr.use_cassette(testfile, record_mode="all"):
# cassette file doesn't exist, so create.
response = urlopen('http://httpbin.org/').read()
with vcr.use_cassette(testfile, record_mode="all") as cass:
# make the same request again
response = urlopen('http://httpbin.org/').read()
# in the "all" record mode, we can add more requests to
# a cassette without repurcussions.
response = urlopen('http://httpbin.org/get').read()
# The cassette was never actually played, even though it existed.
# that's because, in "all" mode, the requests all go directly to
# the source and bypass the cassette.
assert cass.play_count == 0
def test_none_record_mode(tmpdir):
# Cassette file doesn't exist, yet we are trying to make a request.
# raise hell.
testfile = str(tmpdir.join('recordmode.yml'))
with vcr.use_cassette(testfile, record_mode="none"):
with pytest.raises(Exception):
response = urlopen('http://httpbin.org/').read()
def test_none_record_mode_with_existing_cassette(tmpdir):
# create a cassette file
testfile = str(tmpdir.join('recordmode.yml'))
with vcr.use_cassette(testfile, record_mode="all"):
response = urlopen('http://httpbin.org/').read()
# play from cassette file
with vcr.use_cassette(testfile, record_mode="none") as cass:
response = urlopen('http://httpbin.org/').read()
assert cass.play_count == 1
# but if I try to hit the net, raise an exception.
with pytest.raises(Exception):
response = urlopen('http://httpbin.org/get').read()

View File

@@ -0,0 +1,36 @@
import vcr
from six.moves.urllib.request import urlopen
def true_matcher(r1, r2):
return True
def false_matcher(r1, r2):
return False
def test_registered_true_matcher(tmpdir):
my_vcr = vcr.VCR()
my_vcr.register_matcher('true', true_matcher)
testfile = str(tmpdir.join('test.yml'))
with my_vcr.use_cassette(testfile, match_on=['true']) as cass:
# These 2 different urls are stored as the same request
urlopen('http://httpbin.org/')
urlopen('https://httpbin.org/get')
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_false_matcher(tmpdir):
my_vcr = vcr.VCR()
my_vcr.register_matcher('false', false_matcher)
testfile = str(tmpdir.join('test.yml'))
with my_vcr.use_cassette(testfile, match_on=['false']) as cass:
# These 2 different urls are stored as different requests
urlopen('http://httpbin.org/')
urlopen('https://httpbin.org/get')
assert len(cass) == 2

View File

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

View File

@@ -1,11 +1,22 @@
import urllib2
import vcr
from six.moves.urllib.request import urlopen
def test_recorded_request_url_with_redirected_request(tmpdir):
def test_recorded_request_uri_with_redirected_request(tmpdir):
with vcr.use_cassette(str(tmpdir.join('test.yml'))) as cass:
assert len(cass) == 0
urllib2.urlopen('http://httpbin.org/redirect/3')
assert cass.requests[0].url == 'http://httpbin.org/redirect/3'
assert cass.requests[3].url == 'http://httpbin.org/get'
urlopen('http://httpbin.org/redirect/3')
assert cass.requests[0].uri == 'http://httpbin.org/redirect/3'
assert cass.requests[3].uri == 'http://httpbin.org/get'
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 pytest
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")
@@ -20,49 +24,44 @@ def scheme(request):
def test_status_code(scheme, tmpdir):
'''Ensure that we can read the status code'''
url = scheme + '://httpbin.org/'
with vcr.use_cassette(str(tmpdir.join('atts.yaml'))) as cass:
# Ensure that this is empty to begin with
assert_cassette_empty(cass)
assert requests.get(url).status_code == requests.get(url).status_code
# Ensure that we've now cached a single response
assert_cassette_has_one_response(cass)
with vcr.use_cassette(str(tmpdir.join('atts.yaml'))):
status_code = requests.get(url).status_code
with vcr.use_cassette(str(tmpdir.join('atts.yaml'))):
assert status_code == requests.get(url).status_code
def test_headers(scheme, tmpdir):
'''Ensure that we can read the headers back'''
url = scheme + '://httpbin.org/'
with vcr.use_cassette(str(tmpdir.join('headers.yaml'))) as cass:
# Ensure that this is empty to begin with
assert_cassette_empty(cass)
assert requests.get(url).headers == requests.get(url).headers
# Ensure that we've now cached a single response
assert_cassette_has_one_response(cass)
with vcr.use_cassette(str(tmpdir.join('headers.yaml'))):
headers = requests.get(url).headers
with vcr.use_cassette(str(tmpdir.join('headers.yaml'))):
assert headers == requests.get(url).headers
def test_body(tmpdir, scheme):
'''Ensure the responses are all identical enough'''
url = scheme + '://httpbin.org/bytes/1024'
with vcr.use_cassette(str(tmpdir.join('body.yaml'))) as cass:
# Ensure that this is empty to begin with
assert_cassette_empty(cass)
assert requests.get(url).content == requests.get(url).content
# Ensure that we've now cached a single response
assert_cassette_has_one_response(cass)
with vcr.use_cassette(str(tmpdir.join('body.yaml'))):
content = requests.get(url).content
with vcr.use_cassette(str(tmpdir.join('body.yaml'))):
assert content == requests.get(url).content
def test_auth(tmpdir, scheme):
'''Ensure that we can handle basic auth'''
auth = ('user', 'passwd')
url = scheme + '://httpbin.org/basic-auth/user/passwd'
with vcr.use_cassette(str(tmpdir.join('auth.yaml'))) as cass:
# Ensure that this is empty to begin with
assert_cassette_empty(cass)
with vcr.use_cassette(str(tmpdir.join('auth.yaml'))):
one = requests.get(url, auth=auth)
with vcr.use_cassette(str(tmpdir.join('auth.yaml'))):
two = requests.get(url, auth=auth)
assert one.content == two.content
assert one.status_code == two.status_code
# Ensure that we've now cached a single response
assert_cassette_has_one_response(cass)
def test_auth_failed(tmpdir, scheme):
@@ -76,31 +75,29 @@ def test_auth_failed(tmpdir, scheme):
two = requests.get(url, auth=auth)
assert one.content == two.content
assert one.status_code == two.status_code == 401
# Ensure that we've now cached a single response
assert_cassette_has_one_response(cass)
def test_post(tmpdir, scheme):
'''Ensure that we can post and cache the results'''
data = {'key1': 'value1', 'key2': 'value2'}
url = scheme + '://httpbin.org/post'
with vcr.use_cassette(str(tmpdir.join('redirect.yaml'))) as cass:
# Ensure that this is empty to begin with
assert_cassette_empty(cass)
with vcr.use_cassette(str(tmpdir.join('requests.yaml'))):
req1 = requests.post(url, data).content
with vcr.use_cassette(str(tmpdir.join('requests.yaml'))):
req2 = requests.post(url, data).content
assert req1 == req2
# Ensure that we've now cached a single response
assert_cassette_has_one_response(cass)
assert req1 == req2
def test_redirects(tmpdir, scheme):
'''Ensure that we can handle redirects'''
url = scheme + '://httpbin.org/redirect-to?url=bytes/1024'
with vcr.use_cassette(str(tmpdir.join('redirect.yaml'))) as cass:
# Ensure that this is empty to begin with
assert_cassette_empty(cass)
assert requests.get(url).content == requests.get(url).content
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:
assert content == requests.get(url).content
# Ensure that we've now cached *two* responses. One for the redirect
# and one for the final fetch
assert len(cass) == 2
@@ -117,3 +114,94 @@ def test_cross_scheme(tmpdir, scheme):
requests.get('http://httpbin.org/')
assert cass.play_count == 0
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
import os
import urllib2
from urllib import urlencode
import pytest
from six.moves.urllib.request import urlopen
from six.moves.urllib_parse import urlencode
# Internal imports
import vcr
@@ -25,52 +26,44 @@ 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:
# Ensure that this is empty to begin with
assert_cassette_empty(cass)
assert urllib2.urlopen(url).getcode() == urllib2.urlopen(url).getcode()
# Ensure that we've now cached a single response
assert_cassette_has_one_response(cass)
code = urlopen(url).getcode()
with vcr.use_cassette(str(tmpdir.join('atts.yaml'))) as cass:
assert code == urlopen(url).getcode()
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:
# Ensure that this is empty to begin with
assert_cassette_empty(cass)
assert urllib2.urlopen(url).read() == urllib2.urlopen(url).read()
# Ensure that we've now cached a single response
assert_cassette_has_one_response(cass)
body = urlopen(url).read()
with vcr.use_cassette(str(tmpdir.join('body.yaml'))) as cass:
assert body == urlopen(url).read()
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:
# Ensure that this is empty to begin with
assert_cassette_empty(cass)
open1 = urllib2.urlopen(url).info().items()
open2 = urllib2.urlopen(url).info().items()
assert open1 == open2
# Ensure that we've now cached a single response
assert_cassette_has_one_response(cass)
open1 = urlopen(url).info().items()
with vcr.use_cassette(str(tmpdir.join('headers.yaml'))) as cass:
open2 = urlopen(url).info().items()
assert sorted(open1) == sorted(open2)
def test_multiple_requests(scheme, tmpdir):
'''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:
for index in range(len(urls)):
url = urls[index]
assert len(cass) == index
assert cass.play_count == index
assert urllib2.urlopen(url).read() == urllib2.urlopen(url).read()
assert len(cass) == index + 1
assert cass.play_count == index + 1
[urlopen(url) for url in urls]
assert len(cass) == len(urls)
def test_get_data(scheme, tmpdir):
@@ -78,42 +71,38 @@ def test_get_data(scheme, tmpdir):
data = urlencode({'some': 1, 'data': 'here'})
url = scheme + '://httpbin.org/get?' + data
with vcr.use_cassette(str(tmpdir.join('get_data.yaml'))) as cass:
# Ensure that this is empty to begin with
assert_cassette_empty(cass)
res1 = urllib2.urlopen(url).read()
res2 = urllib2.urlopen(url).read()
assert res1 == res2
# Ensure that we've now cached a single response
assert len(cass) == 1
assert cass.play_count == 1
res1 = urlopen(url).read()
with vcr.use_cassette(str(tmpdir.join('get_data.yaml'))) as cass:
res2 = urlopen(url).read()
assert res1 == res2
def test_post_data(scheme, tmpdir):
'''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'
with vcr.use_cassette(str(tmpdir.join('post_data.yaml'))) as cass:
# Ensure that this is empty to begin with
assert_cassette_empty(cass)
res1 = urllib2.urlopen(url, data).read()
res2 = urllib2.urlopen(url, data).read()
assert res1 == res2
# Ensure that we've now cached a single response
assert_cassette_has_one_response(cass)
res1 = urlopen(url, data).read()
with vcr.use_cassette(str(tmpdir.join('post_data.yaml'))) as cass:
res2 = urlopen(url, data).read()
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')})
data = urlencode({'snowman': u''.encode('utf-8')}).encode('utf-8')
url = scheme + '://httpbin.org/post'
with vcr.use_cassette(str(tmpdir.join('post_data.yaml'))) as cass:
# Ensure that this is empty to begin with
assert_cassette_empty(cass)
res1 = urllib2.urlopen(url, data).read()
res2 = urllib2.urlopen(url, data).read()
assert res1 == res2
# Ensure that we've now cached a single response
assert_cassette_has_one_response(cass)
res1 = urlopen(url, data).read()
with vcr.use_cassette(str(tmpdir.join('post_data.yaml'))) as cass:
res2 = urlopen(url, data).read()
assert res1 == res2
assert_cassette_has_one_response(cass)
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
# requests / response pairs in the cassette
with vcr.use_cassette(str(tmpdir.join('cross_scheme.yaml'))) as cass:
urllib2.urlopen('https://httpbin.org/')
urllib2.urlopen('http://httpbin.org/')
urlopen('https://httpbin.org/')
urlopen('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():
return urlopen(url).getcode()
@vcr.use_cassette(str(tmpdir.join('atts.yaml')))
def inner2():
return urlopen(url).getcode()
assert inner1() == inner2()

View File

@@ -3,6 +3,11 @@ requests = pytest.importorskip("requests")
import vcr
try:
import httplib
except ImportError:
import http.client as httplib
def test_domain_redirect():
'''Ensure that redirects across domains are considered unique'''
@@ -15,3 +20,47 @@ def test_domain_redirect():
# Ensure that we've now served two responses. One for the original
# redirect, and a second for the actual fetch
assert len(cass) == 2
def test_flickr_multipart_upload():
"""
The python-flickr-api project does a multipart
upload that confuses vcrpy
"""
def _pretend_to_be_flickr_library():
content_type, body = "text/plain", "HELLO WORLD"
h = httplib.HTTPConnection("httpbin.org")
headers = {
"Content-Type": content_type,
"content-length": str(len(body))
}
h.request("POST", "/post/", headers=headers)
h.send(body)
r = h.getresponse()
data = r.read()
h.close()
with vcr.use_cassette('fixtures/vcr_cassettes/flickr.yaml') as cass:
_pretend_to_be_flickr_library()
assert len(cass) == 1
with vcr.use_cassette('fixtures/vcr_cassettes/flickr.yaml') as cass:
assert len(cass) == 1
_pretend_to_be_flickr_library()
assert cass.play_count == 1
def test_flickr_should_respond_with_200(tmpdir):
testfile = str(tmpdir.join('flickr.yml'))
with vcr.use_cassette(testfile):
r = requests.post("http://api.flickr.com/services/upload")
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,13 +1,22 @@
import copy
from six.moves import http_client as httplib
import contextlib2
import mock
import pytest
import yaml
from vcr.cassette import Cassette
from vcr.patch import force_reset
from vcr.errors import UnhandledHTTPRequestError
def test_cassette_load(tmpdir):
a_file = tmpdir.join('test_cassette.yml')
a_file.write(yaml.dump([
{'request': 'foo', 'response': 'bar'}
]))
a_file.write(yaml.dump({'interactions': [
{'request': {'body': '', 'uri': 'foo', 'method': 'GET', 'headers': {}},
'response': 'bar'}
]}))
a_cassette = Cassette.load(str(a_file))
assert len(a_cassette) == 1
@@ -17,21 +26,6 @@ def test_cassette_not_played():
assert not a.play_count
def test_cassette_played():
a = Cassette('test')
a.mark_played('foo')
a.mark_played('foo')
assert a.play_count == 2
def test_cassette_play_counter():
a = Cassette('test')
a.mark_played('foo')
a.mark_played('bar')
assert a.play_counts['foo'] == 1
assert a.play_counts['bar'] == 1
def test_cassette_append():
a = Cassette('test')
a.append('foo', 'bar')
@@ -46,19 +40,144 @@ def test_cassette_len():
assert len(a) == 2
def _mock_requests_match(request1, request2, matchers):
return request1 == request2
@mock.patch('vcr.cassette.requests_match', _mock_requests_match)
def test_cassette_contains():
a = Cassette('test')
a.append('foo', 'bar')
assert 'foo' in a
def test_cassette_response_of():
@mock.patch('vcr.cassette.requests_match', _mock_requests_match)
def test_cassette_responses_of():
a = Cassette('test')
a.append('foo', 'bar')
assert a.response_of('foo') == 'bar'
assert a.responses_of('foo') == ['bar']
@mock.patch('vcr.cassette.requests_match', _mock_requests_match)
def test_cassette_get_missing_response():
a = Cassette('test')
with pytest.raises(KeyError):
a.response_of('foo')
with pytest.raises(UnhandledHTTPRequestError):
a.responses_of('foo')
@mock.patch('vcr.cassette.requests_match', _mock_requests_match)
def test_cassette_cant_read_same_request_twice():
a = Cassette('test')
a.append('foo', 'bar')
a.play_response('foo')
with pytest.raises(UnhandledHTTPRequestError):
a.play_response('foo')
def make_get_request():
conn = httplib.HTTPConnection("www.python.org")
conn.request("GET", "/index.html")
return conn.getresponse()
@mock.patch('vcr.cassette.requests_match', return_value=True)
@mock.patch('vcr.cassette.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
def test_url():
req = Request('http', 'www.google.com', 80, 'GET', '/', '', {})
assert req.url == 'http://www.google.com/'
def test_str():
req = Request('http', 'www.google.com', 80, 'GET', '/', '', {})
str(req) == '<Request (GET) http://www.google.com>'
req = Request('GET', 'http://www.google.com/', '', {})
str(req) == '<Request (GET) http://www.google.com/>'
def test_headers():
headers = {'X-Header1': ['h1'], 'X-Header2': 'h2'}
req = Request('GET', 'http://go.com/', '', headers)
assert req.headers == {'X-Header1': 'h1', 'X-Header2': 'h2'}
req.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,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

45
tox.ini
View File

@@ -1,35 +1,24 @@
# Tox (http://tox.testrun.org/) is a tool for running tests
# in multiple virtualenvs. This configuration file will run the
# test suite on all supported python versions. To use it, "pip install tox"
# and then run "tox" from this directory.
[tox]
envlist = py26, py27, pypy, py26requests, py27requests, pypyrequests
envlist = {py26,py27,py33,py34,pypy}-{requests24,requests23,requests22,requests1,httplib2,urllib3,boto}
[testenv]
commands =
python setup.py test
py.test {posargs}
basepython =
py26: python2.6
py27: python2.7
py33: python3.3
py34: python3.4
pypy: pypy
deps =
mock
pytest
pytest-localserver
PyYAML
[testenv:py26requests]
basepython = python2.6
deps =
pytest
PyYAML
requests
[testenv:py27requests]
basepython = python2.7
deps =
pytest
PyYAML
requests
[testenv:pypyrequests]
basepython = pypy
deps =
pytest
PyYAML
requests
requests1: requests==1.2.3
requests24: requests==2.4.0
requests23: requests==2.3.0
requests22: requests==2.2.1
httplib2: httplib2
urllib3: urllib3==1.7.1
boto: boto

View File

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

View File

@@ -1,59 +1,187 @@
'''The container for recorded requests and responses'''
"""The container for recorded requests and responses"""
import logging
import contextlib2
import wrapt
try:
from collections import Counter, OrderedDict
from collections import Counter
except ImportError:
from .compat.counter import Counter
from .compat.ordereddict import OrderedDict
# Internal imports
from .patch import install, reset
from .patch import CassettePatcherBuilder
from .persist import load_cassette, save_cassette
from .serializers import yamlserializer
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):
'''A container for recorded requests and responses'''
"""A container for recorded requests and responses"""
@classmethod
def load(cls, path, **kwargs):
'''Load in the cassette stored at the provided path'''
"""Instantiate and load the cassette stored at the specified path."""
new_cassette = cls(path, **kwargs)
new_cassette._load()
return new_cassette
def __init__(self, path, serializer=yamlserializer):
@classmethod
def use_arg_getter(cls, arg_getter):
return CassetteContextDecorator(cls, arg_getter)
@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._serializer = serializer
self.data = OrderedDict()
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 = []
self.play_counts = Counter()
self.dirty = False
self.rewound = False
self.record_mode = record_mode
@property
def play_count(self):
return sum(self.play_counts.values())
@property
def all_played(self):
"""Returns True if all responses have been played, False otherwise."""
return self.play_count == len(self)
@property
def requests(self):
return self.data.keys()
return [request for (request, response) in self.data]
@property
def responses(self):
return self.data.values()
return [response for (request, response) in self.data]
def mark_played(self, request):
'''
Alert the cassette of a request that's been played
'''
self.play_counts[request] += 1
@property
def write_protected(self):
return self.rewound and self.record_mode == 'once' or \
self.record_mode == 'none'
def append(self, request, response):
'''Add a request, response pair to this cassette'''
self.data[request] = response
"""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.dirty = True
def response_of(self, request):
'''Find the response corresponding to a request'''
return self.data[request]
def filter_request(self, request):
return self._before_record_request(request)
def _responses(self, request):
"""
internal API, returns an iterator with all responses matching
the request.
"""
request = self._before_record_request(request)
for index, (stored_request, response) in enumerate(self.data):
if requests_match(request, stored_request, self._match_on):
yield index, response
def can_play_response_for(self, request):
request = self._before_record_request(request)
return request and request in self and \
self.record_mode != 'all' and \
self.rewound
def play_response(self, request):
"""
Get the response corresponding to a request, but only if it
hasn't been played back before, and mark it as played
"""
for index, response in self._responses(request):
if self.play_counts[index] == 0:
self.play_counts[index] += 1
return response
# The cassette doesn't contain the request asked for.
raise UnhandledHTTPRequestError(
"The cassette (%r) doesn't contain the request (%r) asked for"
% (self._path, request)
)
def responses_of(self, request):
"""
Find the responses corresponding to a request.
This function isn't actually used by VCR internally, but is
provided as an external API.
"""
responses = [response for index, response in self._responses(request)]
if responses:
return responses
# The cassette doesn't contain the request asked for.
raise UnhandledHTTPRequestError(
"The cassette (%r) doesn't contain the request (%r) asked for"
% (self._path, request)
)
def _as_dict(self):
return {"requests": self.requests, "responses": self.responses}
@@ -76,6 +204,7 @@ class Cassette(object):
for request, response in zip(requests, responses):
self.append(request, response)
self.dirty = False
self.rewound = True
except IOError:
pass
@@ -85,18 +214,11 @@ class Cassette(object):
)
def __len__(self):
'''Return the number of request,response pairs stored in here'''
"""Return the number of request,response pairs stored in here"""
return len(self.data)
def __contains__(self, request):
'''Return whether or not a request has been stored'''
return request in self.data
def __enter__(self):
'''Patch the fetching libraries we know about'''
install(self)
return self
def __exit__(self, typ, value, traceback):
self._save()
reset()
"""Return whether or not a request has been stored"""
for response in self._responses(request):
return True
return False

View File

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

View File

@@ -1,42 +1,170 @@
import collections
import copy
import functools
import os
from .cassette import Cassette
from .serializers import yamlserializer, jsonserializer
from . import matchers
from . import filters
class VCR(object):
def __init__(self, serializer='yaml', cassette_library_dir=None):
def __init__(self, serializer='yaml', cassette_library_dir=None,
record_mode="once", filter_headers=(),
filter_query_parameters=(), before_record_request=None,
before_record_response=None, ignore_hosts=(),
match_on=('method', 'scheme', 'host', 'port', 'path', 'query',),
ignore_localhost=False, before_record=None):
self.serializer = serializer
self.match_on = match_on
self.cassette_library_dir = cassette_library_dir
self.serializers = {
'yaml': yamlserializer,
'json': jsonserializer,
}
self.matchers = {
'method': matchers.method,
'uri': matchers.uri,
'url': matchers.uri, # matcher for backwards compatibility
'scheme': matchers.scheme,
'host': matchers.host,
'port': matchers.port,
'path': matchers.path,
'query': matchers.query,
'headers': matchers.headers,
'body': matchers.body,
}
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):
try:
serializer = self.serializers[serializer_name]
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
)
))
raise KeyError
return serializer
def use_cassette(self, path, **kwargs):
def _get_matchers(self, matcher_names):
matchers = []
try:
for m in matcher_names:
matchers.append(self.matchers[m])
except KeyError:
raise KeyError(
"Matcher {0} doesn't exist or isn't registered".format(m)
)
return matchers
def use_cassette(self, path, 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)
matcher_names = kwargs.get('match_on', self.match_on)
cassette_library_dir = kwargs.get(
'cassette_library_dir',
self.cassette_library_dir
)
if cassette_library_dir:
path = os.path.join(cassette_library_dir, path)
merged_config = {
"serializer": self._get_serializer(serializer_name),
'serializer': self._get_serializer(serializer_name),
'match_on': self._get_matchers(matcher_names),
'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):
self.serializers[name] = serializer
def register_matcher(self, name, matcher):
self.matchers[name] = matcher

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

53
vcr/matchers.py Normal file
View File

@@ -0,0 +1,53 @@
import logging
log = logging.getLogger(__name__)
def method(r1, r2):
return r1.method == r2.method
def uri(r1, r2):
return r1.uri == r2.uri
def host(r1, r2):
return r1.host == r2.host
def scheme(r1, r2):
return r1.scheme == r2.scheme
def port(r1, r2):
return r1.port == r2.port
def path(r1, r2):
return r1.path == r2.path
def query(r1, r2):
return r1.query == r2.query
def body(r1, r2):
return r1.body == r2.body
def headers(r1, r2):
return r1.headers == r2.headers
def _log_matches(r1, r2, matches):
differences = [m for m in matches if not m[0]]
if differences:
log.debug(
"Requests {0} and {1} differ according to "
"the following matchers: {2}".format(r1, r2, differences)
)
def requests_match(r1, r2, matchers):
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,75 +1,305 @@
'''Utilities for patching in cassettes'''
import functools
import itertools
import contextlib2
import mock
import httplib
from .stubs import VCRHTTPConnection, VCRHTTPSConnection
from six.moves import http_client as httplib
# Save some of the original types for the purposes of unpatching
_HTTPConnection = httplib.HTTPConnection
_HTTPSConnection = httplib.HTTPSConnection
# Try to save the original types for requests
try:
# Try to save the original types for requests
import requests.packages.urllib3.connectionpool as cpool
except ImportError: # pragma: no cover
pass
else:
_VerifiedHTTPSConnection = cpool.VerifiedHTTPSConnection
_HTTPConnection = cpool.HTTPConnection
except ImportError: # pragma: no cover
pass
_cpoolHTTPConnection = cpool.HTTPConnection
_cpoolHTTPSConnection = cpool.HTTPSConnection
# Try to save the original types for urllib3
try:
# Try to save the original types for urllib3
import urllib3
_VerifiedHTTPSConnection = urllib3.connectionpool.VerifiedHTTPSConnection
except ImportError: # pragma: no cover
pass
else:
_VerifiedHTTPSConnection = urllib3.connectionpool.VerifiedHTTPSConnection
def install(cassette):
'''Install a cassette in lieu of actuall fetching'''
httplib.HTTPConnection = httplib.HTTP._connection_class = VCRHTTPConnection
httplib.HTTPSConnection = httplib.HTTPS._connection_class = (
VCRHTTPSConnection)
httplib.HTTPConnection.cassette = cassette
httplib.HTTPSConnection.cassette = cassette
# Try to save the original types for httplib2
try:
import httplib2
except ImportError: # pragma: no cover
pass
else:
_HTTPConnectionWithTimeout = httplib2.HTTPConnectionWithTimeout
_HTTPSConnectionWithTimeout = httplib2.HTTPSConnectionWithTimeout
_SCHEME_TO_CONNECTION = httplib2.SCHEME_TO_CONNECTION
# 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.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 not pool.pool.empty() and connections:
connection = pool.pool.get()
if isinstance(connection, self._connection_class):
connections.remove(connection)
else:
readd_connections.append(connection)
for connection in readd_connections:
pool._put_conn(connection)
def reset_patchers():
yield mock.patch.object(httplib, 'HTTPConnection', _HTTPConnection)
yield mock.patch.object(httplib, 'HTTPSConnection', _HTTPSConnection)
try:
import requests.packages.urllib3.connectionpool as cpool
from .stubs.requests_stubs import VCRVerifiedHTTPSConnection
cpool.VerifiedHTTPSConnection = VCRVerifiedHTTPSConnection
cpool.VerifiedHTTPSConnection.cassette = cassette
cpool.HTTPConnection = VCRHTTPConnection
cpool.HTTPConnection.cassette = cassette
except ImportError: # pragma: no cover
pass
else:
# unpatch requests v1.x
yield mock.patch.object(cpool, 'VerifiedHTTPSConnection', _VerifiedHTTPSConnection)
yield mock.patch.object(cpool, 'HTTPConnection', _cpoolHTTPConnection)
# unpatch requests v2.x
if hasattr(cpool.HTTPConnectionPool, 'ConnectionCls'):
yield mock.patch.object(cpool.HTTPConnectionPool, 'ConnectionCls',
_cpoolHTTPConnection)
yield mock.patch.object(cpool.HTTPSConnectionPool, 'ConnectionCls',
_cpoolHTTPSConnection)
# patch urllib3
try:
import urllib3.connectionpool as cpool
from .stubs.urllib3_stubs import VCRVerifiedHTTPSConnection
cpool.VerifiedHTTPSConnection = VCRVerifiedHTTPSConnection
cpool.VerifiedHTTPSConnection.cassette = cassette
cpool.HTTPConnection = VCRHTTPConnection
cpool.HTTPConnection.cassette = cassette
except ImportError: # pragma: no cover
pass
def reset():
'''Undo all the patching'''
httplib.HTTPConnection = httplib.HTTP._connection_class = _HTTPConnection
httplib.HTTPSConnection = httplib.HTTPS._connection_class = \
_HTTPSConnection
try:
import requests.packages.urllib3.connectionpool as cpool
cpool.VerifiedHTTPSConnection = _VerifiedHTTPSConnection
cpool.HTTPConnection = _HTTPConnection
except ImportError: # pragma: no cover
pass
if hasattr(cpool, 'HTTPSConnection'):
yield mock.patch.object(cpool, 'HTTPSConnection', _cpoolHTTPSConnection)
try:
import urllib3.connectionpool as cpool
cpool.VerifiedHTTPSConnection = _VerifiedHTTPSConnection
cpool.HTTPConnection = _HTTPConnection
except ImportError: # pragma: no cover
pass
else:
yield mock.patch.object(cpool, 'VerifiedHTTPSConnection', _VerifiedHTTPSConnection)
yield mock.patch.object(cpool, 'HTTPConnection', _HTTPConnection)
yield mock.patch.object(cpool, 'HTTPSConnection', _HTTPSConnection)
if hasattr(cpool.HTTPConnectionPool, 'ConnectionCls'):
yield mock.patch.object(cpool.HTTPConnectionPool, 'ConnectionCls', _HTTPConnection)
yield mock.patch.object(cpool.HTTPSConnectionPool, 'ConnectionCls', _HTTPSConnection)
try:
import 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 .serialize import serialize, deserialize
def load_cassette(cassette_path, serializer):
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):
data = serializer.serialize(cassette_dict)
data = serialize(cassette_dict, serializer)
FilesystemPersister.write(cassette_path, data)

View File

@@ -1,23 +1,11 @@
import tempfile
import os
class FilesystemPersister(object):
@classmethod
def _secure_write(cls, path, contents):
"""
We'll overwrite the old version securely by writing out a temporary
version and then moving it to replace the old version
"""
dirname, filename = os.path.split(path)
fd, name = tempfile.mkstemp(dir=dirname, prefix=filename)
with os.fdopen(fd, 'w') as fout:
fout.write(contents)
os.rename(name, path)
@classmethod
def write(cls, cassette_path, data):
dirname, filename = os.path.split(cassette_path)
if dirname and not os.path.exists(dirname):
os.makedirs(dirname)
cls._secure_write(cassette_path, data)
with open(cassette_path, 'w') as f:
f.write(data)

View File

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

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

View File

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

View File

@@ -1,123 +1,316 @@
'''Stubs for patching HTTP and HTTPS requests'''
from httplib import HTTPConnection, HTTPSConnection, HTTPMessage
from cStringIO import StringIO
try:
import http.client
except ImportError:
pass
import logging
import six
from six.moves.http_client import (
HTTPConnection,
HTTPSConnection,
HTTPMessage,
HTTPResponse,
)
from six import BytesIO
from vcr.request import Request
from vcr.errors import CannotOverwriteExistingCassetteException
from . import compat
log = logging.getLogger(__name__)
class VCRHTTPResponse(object):
class VCRFakeSocket(object):
"""
A socket that doesn't do anything!
Used when playing back casssettes, when there
is no actual open socket.
"""
def close(self):
pass
def settimeout(self, *args, **kwargs):
pass
def fileno(self):
"""
This is kinda crappy. requests will watch
this descriptor and make sure it's not closed.
Return file descriptor 0 since that's stdin.
"""
return 0 # wonder how bad this is....
def parse_headers(header_list):
"""
Convert headers from our serialized dict with lists for keys to a
HTTPMessage
"""
header_string = b""
for key, values in header_list.items():
for v in values:
header_string += \
key.encode('utf-8') + b":" + v.encode('utf-8') + b"\r\n"
return compat.get_httpmessage(header_string)
def serialize_headers(response):
out = {}
for key, values in compat.get_headers(response.msg):
out.setdefault(key, [])
out[key].extend(values)
return out
class VCRHTTPResponse(HTTPResponse):
"""
Stub reponse class that gets returned instead of a HTTPResponse
"""
def __init__(self, recorded_response):
self.recorded_response = recorded_response
self.reason = recorded_response['status']['message']
self.status = recorded_response['status']['code']
self.status = self.code = recorded_response['status']['code']
self.version = None
self._content = StringIO(self.recorded_response['body']['string'])
self._content = BytesIO(self.recorded_response['body']['string'])
self._closed = False
# We are skipping the header parsing (they have already been parsed
# at this point) and directly adding the headers to the header
# container, so just pass an empty StringIO.
self.msg = HTTPMessage(StringIO(''))
headers = self.recorded_response['headers']
self.msg = parse_headers(headers)
for key, val in self.recorded_response['headers'].iteritems():
self.msg.addheader(key, val)
# msg.addheaders adds the headers to msg.dict, but not to
# the msg.headers list representation of headers, so
# I have to add it to both.
self.msg.headers.append("{0}:{1}".format(key, val))
self.length = compat.get_header(self.msg, 'content-length') or None
self.length = self.msg.getheader('content-length') or None
@property
def closed(self):
# in python3, I can't change the value of self.closed. So I'
# twiddling self._closed and using this property to shadow the real
# self.closed from the superclas
return self._closed
def read(self, *args, **kwargs):
# Note: I'm pretty much ignoring any chunking stuff because
# I don't really understand what it is or how it works.
return self._content.read(*args, **kwargs)
def readline(self, *args, **kwargs):
return self._content.readline(*args, **kwargs)
def close(self):
self._closed = True
return True
def getcode(self):
return self.status
def isclosed(self):
# Urllib3 seems to call this because it actually uses
# the weird chunking support in httplib
return True
return self.closed
def info(self):
return parse_headers(self.recorded_response['headers'])
def getheaders(self):
return self.recorded_response['headers'].iteritems()
message = parse_headers(self.recorded_response['headers'])
return list(compat.get_header_items(message))
def getheader(self, header, default=None):
values = [v for (k, v) in self.getheaders() if k.lower() == header.lower()]
if values:
return ', '.join(values)
else:
return default
class VCRConnectionMixin:
class VCRConnection(object):
# A reference to the cassette that's currently being patched in
cassette = None
def _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):
'''Persist the request metadata in self._vcr_request'''
self._vcr_request = Request(
protocol=self._protocol,
host=self.host,
port=self.port,
method=method,
path=url,
uri=self._uri(url),
body=body,
headers=headers or {}
)
log.debug('Got {0}'.format(self._vcr_request))
# Check if we have a cassette set, and if we have a response saved.
# If so, there's no need to keep processing and we can bail
if self.cassette and self._vcr_request in self.cassette:
return
# Note: The request may not actually be finished at this point, so
# I'm not sending the actual request until getresponse(). This
# allows me to compare the entire length of the response to see if it
# exists in the cassette.
# Otherwise, we should submit the request
self._baseclass.request(
self, method, url, body=body, headers=headers or {})
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):
'''
This method is called after request(), to add additional data to the
body of the request. So if that happens, let's just append the data
onto the most recent request in the cassette.
'''
self._vcr_request.body = (self._vcr_request.body or '') + data
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):
"""
Normally, this would atually send the request to the server.
We are not sending the request until getting the response,
so bypass this method for now.
"""
pass
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,
# then return it
if self._vcr_request in self.cassette:
response = self.cassette.response_of(self._vcr_request)
# Alert the cassette to the fact that we've served another
# response for the provided requests
self.cassette.mark_played(self._vcr_request)
if self.cassette.can_play_response_for(self._vcr_request):
log.info(
"Playing response for {0} from cassette".format(
self._vcr_request
)
)
response = self.cassette.play_response(self._vcr_request)
return VCRHTTPResponse(response)
else:
# Otherwise, we made an actual request, and should return the
# response we got from the actual connection
response = HTTPConnection.getresponse(self)
if self.cassette.write_protected and self.cassette.filter_request(
self._vcr_request
):
raise CannotOverwriteExistingCassetteException(
"No match for the request (%r) was found. "
"Can't overwrite existing cassette (%r) in "
"your current record mode (%r)."
% (self._vcr_request, self.cassette._path,
self.cassette.record_mode)
)
# Otherwise, we should send the request, then get the response
# and return it.
log.info(
"{0} not in cassette, sending to real server".format(
self._vcr_request
)
)
self.real_connection.request(
method=self._vcr_request.method,
url=self._url(self._vcr_request.uri),
body=self._vcr_request.body,
headers=self._vcr_request.headers,
)
# get the response
response = self.real_connection.getresponse()
# put the response into the cassette
response = {
'status': {
'code': response.status,
'message': response.reason
},
'headers': dict(response.getheaders()),
'headers': serialize_headers(response),
'body': {'string': response.read()},
}
self.cassette.append(self._vcr_request, response)
return VCRHTTPResponse(response)
def set_debuglevel(self, *args, **kwargs):
self.real_connection.set_debuglevel(*args, **kwargs)
class VCRHTTPConnection(VCRConnectionMixin, HTTPConnection):
def connect(self, *args, **kwargs):
"""
httplib2 uses this. Connects to the server I'm assuming.
Only pass to the baseclass if we don't have a recorded response
and are not write-protected.
"""
if hasattr(self, '_vcr_request') and \
self.cassette.can_play_response_for(self._vcr_request):
# We already have a response we are going to play, don't
# actually connect
return
if self.cassette.write_protected:
# Cassette is write-protected, don't actually connect
return
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'''
# Can't use super since this is an old-style class
_baseclass = HTTPConnection
_protocol = 'http'
def __init__(self, *args, **kwargs):
HTTPConnection.__init__(self, *args, **kwargs)
class VCRHTTPSConnection(VCRConnectionMixin, HTTPSConnection):
class VCRHTTPSConnection(VCRConnection):
'''A Mocked class for HTTPS requests'''
_baseclass = HTTPSConnection
_protocol = 'https'
def __init__(self, *args, **kwargs):
'''I overrode the init and copied a lot of the code from the parent
class because HTTPConnection when this happens has been replaced by
VCRHTTPConnection, but doing it here lets us use the original one.'''
HTTPConnection.__init__(self, *args, **kwargs)
self.key_file = kwargs.pop('key_file', None)
self.cert_file = kwargs.pop('cert_file', None)

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'''
from requests.packages.urllib3.connectionpool import VerifiedHTTPSConnection
from ..stubs import VCRHTTPSConnection
from requests.packages.urllib3.connectionpool import HTTPConnection, VerifiedHTTPSConnection
from ..stubs import VCRHTTPConnection, VCRHTTPSConnection
# urllib3 defines its own HTTPConnection classes, which requests goes ahead and assumes
# you're using. It includes some polyfills for newer features missing in older pythons.
class VCRVerifiedHTTPSConnection(VCRHTTPSConnection, VerifiedHTTPSConnection):
class VCRRequestsHTTPConnection(VCRHTTPConnection, HTTPConnection):
_baseclass = HTTPConnection
class VCRRequestsHTTPSConnection(VCRHTTPSConnection, VerifiedHTTPSConnection):
_baseclass = VerifiedHTTPSConnection