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

Compare commits

..

55 Commits

Author SHA1 Message Date
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
33 changed files with 1243 additions and 495 deletions

View File

@@ -1,13 +1,29 @@
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.x"
- 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
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
- pypy
install:
- pip install PyYAML pytest --use-mirrors
- if [ $WITH_LIB = "requests1.x" ] ; then pip install requests==1.2.3; fi
- if [ $WITH_LIB = "requests2.x" ] ; then pip install requests; 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 @@
#VCR.py
# VCR.py
![vcr.py](https://raw.github.com/kevin1024/vcrpy/master/vcr.png)
@@ -6,7 +6,7 @@ This is a Python version of [Ruby's VCR library](https://github.com/myronmarston
[![Build Status](https://secure.travis-ci.org/kevin1024/vcrpy.png?branch=master)](http://travis-ci.org/kevin1024/vcrpy)
##What it does
## 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
@@ -17,12 +17,18 @@ 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, 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 +38,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 +46,21 @@ 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.
`VCR` class and setting the options on it.
```python
@@ -100,7 +117,7 @@ VCR supports 4 record modes (with the same behavior as Ruby's VCR):
* 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,
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).
@@ -153,7 +170,7 @@ with vcr.use_cassette('fixtures/vcr_cassettes/synopsis.yaml') as cass:
assert cass.requests[0].url == 'http://www.zombo.com/'
```
The Cassette object exposes the following properties which I consider
The `Cassette` object exposes the following properties which I consider
part of the API. The fields are as follows:
* `requests`: A list of vcr.Request objects containing the requests made
@@ -162,11 +179,9 @@ part of the API. The fields are as follows:
* `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.
* `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"
@@ -219,8 +234,8 @@ Create your own method with the following signature
def my_matcher(r1, r2):
```
Your method receives the two requests and must return True if they
match, False if they don't.
Your method receives the two requests and must return `True` if they
match, `False` if they don't.
Finally, register your method with VCR to use your
new request matcher.
@@ -246,19 +261,38 @@ with my_vcr.use_cassette('test.yml'):
```
##Installation
## 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
## Ruby VCR compatibility
I'm not trying to match the format of the Ruby VCR YAML files. Cassettes generated by
Ruby's VCR are not compatible with VCR.py.
##Known Issues
This library is a work in progress, so the API might change on you.
There are probably some [bugs](https://github.com/kevin1024/vcrpy/issues?labels=bug&page=1&state=open) floating around too.
## Running VCR's test suite
##Changelog
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.
## Changelog
* 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 bobo 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
@@ -286,18 +320,5 @@ There are probably some [bugs](https://github.com/kevin1024/vcrpy/issues?labels=
* 0.0.3: Add support for requests 1.2.3. Support for older versions of requests dropped (thanks @vitormazzi and @bryanhelmig)
* 0.0.2: Add support for requests / urllib3
* 0.0.1: Initial Release
##Similar libraries in Python
Neither of these really implement the API I want, but I have cribbed some code
from them.
* https://github.com/bbangert/Dalton
* https://github.com/storborg/replaylib
These were created after I created VCR.py but do something similar:
* https://github.com/gabrielfalcao/HTTPretty
* https://github.com/kanzure/python-requestions
* https://github.com/uber/cassette
#License
# License
This library uses the MIT license. See [LICENSE.txt](LICENSE.txt) for more details

View File

@@ -19,8 +19,8 @@ class PyTest(TestCommand):
sys.exit(errno)
setup(name='vcrpy',
version='0.3.0',
description="A Python port of Ruby's VCR to make mocking HTTP easier",
version='0.7.0',
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',
@@ -37,7 +37,7 @@ setup(name='vcrpy',
'vcr.compat': 'vcr/compat',
'vcr.persisters': 'vcr/persisters',
},
install_requires=['PyYAML'],
install_requires=['PyYAML','contextdecorator','six'],
license='MIT',
tests_require=['pytest','mock'],
cmdclass={'test': PyTest},
@@ -46,6 +46,7 @@ setup(name='vcrpy',
'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

@@ -1,3 +1,19 @@
- 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: ''}
headers: ["Location: http://moz.com/\r\n", "Server: BigIP\r\n", "Connection: Keep-Alive\r\n",
"Content-Length: 0\r\n"]
status: {code: 301, message: Moved Permanently}
- request: !!python/object:vcr.request.Request
body: null
headers: !!python/object/apply:__builtin__.frozenset
@@ -12,120 +28,110 @@
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\r\n", "Content-Type: text/html\r\n", "Vary: Accept-Encoding\r\n",
"Cache-Control: no-cache, must-revalidate, s-maxage=3600\r\n", "Expires: Fri,
15 Oct 2004 12:00:00 GMT\r\n", "Server-Name: dalmozwww01.dal.moz.com\r\n",
"Content-Encoding: gzip\r\n", "Content-Length: 5683\r\n", "Accept-Ranges: bytes\r\n",
"Date: Sat, 11 Jan 2014 18:45:11 GMT\r\n", "X-Varnish: 918768771 918700396\r\n",
"Age: 3479\r\n", "Via: 1.1 varnish\r\n", "Connection: keep-alive\r\n"]
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

@@ -3,7 +3,7 @@
# 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,41 @@
import pytest
boto = pytest.importorskip("boto")
from boto.s3.connection import S3Connection
from boto.s3.key import Key
import vcr
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')

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)
@@ -46,8 +46,8 @@ def test_disk_saver_write(tmpdir):
time.sleep(1)
with vcr.use_cassette(fname, record_mode='any') as cass:
urllib2.urlopen('http://www.iana.org/domains/reserved').read()
urllib2.urlopen('http://httpbin.org/').read()
urlopen('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,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,20 @@
import pytest
import vcr
from six.moves.urllib.request import urlopen
def test_making_extra_request_raises_exception(tmpdir):
# make two requests in the first request that are considered
# identical (since the match is based on method)
with vcr.use_cassette(str(tmpdir.join('test.json')), match_on=['method']):
urlopen('http://httpbin.org/status/200')
urlopen('http://httpbin.org/status/201')
# Now, try to make three requests. The first two should return the
# correct status codes in order, and the third should raise an
# exception.
with vcr.use_cassette(str(tmpdir.join('test.json')), match_on=['method']):
assert urlopen('http://httpbin.org/status/200').getcode() == 200
assert urlopen('http://httpbin.org/status/201').getcode() == 201
with pytest.raises(Exception):
urlopen('http://httpbin.org/status/200')

View File

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

View File

@@ -1,5 +1,5 @@
import urllib2
import vcr
from six.moves.urllib.request import urlopen
def true_matcher(r1, r2):
@@ -16,9 +16,13 @@ def test_registered_serializer_true_matcher(tmpdir):
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
urllib2.urlopen('http://httpbin.org/')
urllib2.urlopen('https://httpbin.org/get')
assert len(cass) == 1
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_serializer_false_matcher(tmpdir):
@@ -27,6 +31,6 @@ def test_registered_serializer_false_matcher(tmpdir):
testfile = str(tmpdir.join('test.yml'))
with my_vcr.use_cassette(testfile, match_on=['false']) as cass:
# These 2 different urls are stored as different requests
urllib2.urlopen('http://httpbin.org/')
urllib2.urlopen('https://httpbin.org/get')
urlopen('http://httpbin.org/')
urlopen('https://httpbin.org/get')
assert len(cass) == 2

View File

@@ -1,4 +1,3 @@
import urllib2
import vcr
@@ -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,11 @@
import urllib2
import vcr
from six.moves.urllib.request import urlopen
def test_recorded_request_url_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')
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'
assert len(cass) == 4

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")
@@ -21,33 +25,30 @@ 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)
status_code = requests.get(url).status_code
with vcr.use_cassette(str(tmpdir.join('atts.yaml'))) as cass:
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)
headers = requests.get(url).headers
with vcr.use_cassette(str(tmpdir.join('headers.yaml'))) as cass:
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)
content = requests.get(url).content
with vcr.use_cassette(str(tmpdir.join('body.yaml'))) as cass:
assert content == requests.get(url).content
def test_auth(tmpdir, scheme):
@@ -55,14 +56,12 @@ def test_auth(tmpdir, scheme):
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)
one = requests.get(url, auth=auth)
with vcr.use_cassette(str(tmpdir.join('auth.yaml'))) as cass:
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,8 +75,6 @@ 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):
@@ -85,22 +82,22 @@ def test_post(tmpdir, scheme):
data = {'key1': 'value1', 'key2': 'value2'}
url = scheme + '://httpbin.org/post'
with vcr.use_cassette(str(tmpdir.join('requests.yaml'))) as cass:
# Ensure that this is empty to begin with
assert_cassette_empty(cass)
req1 = requests.post(url, data).content
with vcr.use_cassette(str(tmpdir.join('requests.yaml'))) as cass:
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('requests.yaml'))) as cass:
# Ensure that this is empty to begin with
assert_cassette_empty(cass)
assert requests.get(url).content == requests.get(url).content
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,34 @@ 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'))) as cass:
response = requests.get(url)
assert_is_json(response.content)
with vcr.use_cassette(str(tmpdir.join('gzip.yaml'))) as cass:
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()
resp = session.get('http://httpbin.org/get', headers={'Connection': 'close'})
resp = session.get('http://httpbin.org/get', headers={'Connection': 'close'})

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()
open1 = urlopen(url).info().items()
with vcr.use_cassette(str(tmpdir.join('headers.yaml'))) as cass:
open2 = urlopen(url).info().items()
assert open1 == open2
# Ensure that we've now cached a single response
assert_cassette_has_one_response(cass)
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,7 +3,10 @@ requests = pytest.importorskip("requests")
import vcr
import httplib
try:
import httplib
except ImportError:
import http.client as httplib
def test_domain_redirect():
@@ -45,3 +48,27 @@ def test_flickr_multipart_upload():
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 x_test_zip_file(tmpdir):
# TODO: How do I make this pass?
zipfile = "http://www.colorado.edu/conflict/peace/download/peace_example.ZIP"
testfile = str(tmpdir.join('test.json'))
with vcr.use_cassette(testfile, serializer='json'):
r = requests.post(zipfile)
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

@@ -2,6 +2,7 @@ import pytest
import yaml
import mock
from vcr.cassette import Cassette
from vcr.errors import UnhandledHTTPRequestError
def test_cassette_load(tmpdir):
@@ -18,21 +19,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')
@@ -50,6 +36,7 @@ def test_cassette_len():
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')
@@ -58,14 +45,22 @@ def test_cassette_contains():
@mock.patch('vcr.cassette.requests_match', _mock_requests_match)
def test_cassette_response_of():
def test_cassette_responses_of():
a = Cassette('test')
a.append('foo', 'bar')
assert a.response_of('foo') == 'bar'
assert a.responses_of('foo') == ['bar']
@mock.patch('vcr.cassette.requests_match', _mock_requests_match)
def test_cassette_get_missing_response():
a = Cassette('test')
with pytest.raises(KeyError):
a.response_of('foo')
with pytest.raises(UnhandledHTTPRequestError):
a.responses_of('foo')
@mock.patch('vcr.cassette.requests_match', _mock_requests_match)
def test_cassette_cant_read_same_request_twice():
a = Cassette('test')
a.append('foo','bar')
a.play_response('foo')
with pytest.raises(UnhandledHTTPRequestError):
a.play_response('foo')

96
tox.ini
View File

@@ -4,18 +4,68 @@
# and then run "tox" from this directory.
[tox]
envlist = py26, py27, pypy, py26requests, py27requests, pypyrequests
envlist =
py26
py27
py33
pypy
py26requests
py27requests
py33requests
pypyrequests
py26oldrequests
py27oldrequests
py33oldrequests
pypyoldrequests
py26httplib2
py27httplib2
py33httplib2
pypyhttplib2
[testenv]
commands =
python setup.py test
py.test {posargs}
deps =
mock
pytest
PyYAML
[testenv:py26oldrequests]
basepython = python2.6
deps =
mock
pytest
PyYAML
requests==1.2.3
[testenv:py27oldrequests]
basepython = python2.7
deps =
mock
pytest
PyYAML
requests==1.2.3
[testenv:py33oldrequests]
basepython = python3.3
deps =
mock
pytest
PyYAML
requests==1.2.3
[testenv:pypyoldrequests]
basepython = pypy
deps =
mock
pytest
PyYAML
requests==1.2.3
[testenv:py26requests]
basepython = python2.6
deps =
mock
pytest
PyYAML
requests
@@ -23,6 +73,15 @@ deps =
[testenv:py27requests]
basepython = python2.7
deps =
mock
pytest
PyYAML
requests
[testenv:py33requests]
basepython = python3.3
deps =
mock
pytest
PyYAML
requests
@@ -30,6 +89,39 @@ deps =
[testenv:pypyrequests]
basepython = pypy
deps =
mock
pytest
PyYAML
requests
[testenv:py26httplib2]
basepython = python2.6
deps =
mock
pytest
PyYAML
httplib2
[testenv:py27httplib2]
basepython = python2.7
deps =
mock
pytest
PyYAML
httplib2
[testenv:py33httplib2]
basepython = python3.3
deps =
mock
pytest
PyYAML
httplib2
[testenv:pypyhttplib2]
basepython = pypy
deps =
mock
pytest
PyYAML
httplib2

View File

@@ -1,8 +1,7 @@
from config import VCR
from .config import VCR
default_vcr = VCR()
# Also, make a 'load' function available
def use_cassette(path, **kwargs):
return default_vcr.use_cassette(path, **kwargs)

View File

@@ -6,14 +6,17 @@ except ImportError:
from .compat.counter import Counter
from .compat.ordereddict import OrderedDict
from contextdecorator import ContextDecorator
# Internal imports
from .patch import install, reset
from .persist import load_cassette, save_cassette
from .serializers import yamlserializer
from .matchers import requests_match, url, method
from .errors import UnhandledHTTPRequestError
class Cassette(object):
class Cassette(ContextDecorator):
'''A container for recorded requests and responses'''
@classmethod
@@ -36,6 +39,7 @@ class Cassette(object):
self.data = []
self.play_counts = Counter()
self.dirty = False
self.rewound = False
self.record_mode = record_mode
@property
@@ -50,47 +54,49 @@ class Cassette(object):
def responses(self):
return [response for (request, response) in self.data]
@property
def rewound(self):
"""
If the cassette has already been recorded in another session, and has
been loaded again fresh from disk, it has been "rewound". This means
that it should be write-only, depending on the record mode specified
"""
return not self.dirty and self.play_count
@property
def write_protected(self):
return self.rewound and self.record_mode == 'once' or \
self.record_mode == 'none'
def mark_played(self, request):
'''
Alert the cassette of a request that's been played
'''
self.play_counts[request] += 1
def append(self, request, response):
'''Add a request, response pair to this cassette'''
self.data.append((request, response))
self.dirty = True
def response_of(self, request):
def play_response(self, request):
'''
Find the response corresponding to a request
Get the response corresponding to a request, but only if it
hasn't been played back before, and mark it as played
'''
responses = []
for stored_request, response in self.data:
for index, (stored_request, response) in enumerate(self.data):
if requests_match(request, stored_request, self._match_on):
responses.append(response)
index = self.play_counts[request]
try:
return responses[index]
except IndexError:
# I decided that a KeyError is the best exception to raise
# if the cassette doesn't contain the request asked for.
raise KeyError
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 = \
[resp for req, resp in self.data if
requests_match(req, request, self._match_on)]
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}
@@ -113,6 +119,7 @@ class Cassette(object):
for request, response in zip(requests, responses):
self.append(request, response)
self.dirty = False
self.rewound = True
except IOError:
pass

View File

@@ -8,9 +8,11 @@ class VCR(object):
def __init__(self,
serializer='yaml',
cassette_library_dir=None,
record_mode="once"):
record_mode="once",
match_on=['url', 'method'],
):
self.serializer = serializer
self.match_on = ['url', 'method']
self.match_on = match_on
self.cassette_library_dir = cassette_library_dir
self.serializers = {
'yaml': yamlserializer,
@@ -30,20 +32,22 @@ class VCR(object):
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 _get_matchers(self, matcher_names):
matchers = []
try:
matchers = [self.matchers[m] for m in matcher_names]
for m in matcher_names:
matchers.append(self.matchers[m])
except KeyError:
print "Matcher {0} doesn't exist or isn't registered".format(
matcher_name
raise KeyError(
"Matcher {0} doesn't exist or isn't registered".format(
m)
)
raise KeyError
return matchers
def use_cassette(self, path, **kwargs):

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

View File

@@ -1,7 +1,7 @@
'''Utilities for patching in cassettes'''
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
@@ -12,7 +12,8 @@ try:
# Try to save the original types for requests
import requests.packages.urllib3.connectionpool as cpool
_VerifiedHTTPSConnection = cpool.VerifiedHTTPSConnection
_HTTPConnection = cpool.HTTPConnection
_cpoolHTTPConnection = cpool.HTTPConnection
_cpoolHTTPSConnection = cpool.HTTPSConnection
except ImportError: # pragma: no cover
pass
@@ -23,6 +24,15 @@ try:
except ImportError: # pragma: no cover
pass
try:
# Try to save the original types for httplib2
import httplib2
_HTTPConnectionWithTimeout = httplib2.HTTPConnectionWithTimeout
_HTTPSConnectionWithTimeout = httplib2.HTTPSConnectionWithTimeout
_SCHEME_TO_CONNECTION = httplib2.SCHEME_TO_CONNECTION
except ImportError: # pragma: no cover
pass
def install(cassette):
"""
@@ -30,13 +40,12 @@ def install(cassette):
This replaces the actual HTTPConnection with a VCRHTTPConnection
object which knows how to save to / read from cassettes
"""
httplib.HTTPConnection = httplib.HTTP._connection_class = VCRHTTPConnection
httplib.HTTPSConnection = httplib.HTTPS._connection_class = (
VCRHTTPSConnection)
httplib.HTTPConnection = VCRHTTPConnection
httplib.HTTPSConnection = VCRHTTPSConnection
httplib.HTTPConnection.cassette = cassette
httplib.HTTPSConnection.cassette = cassette
# patch requests
# patch requests v1.x
try:
import requests.packages.urllib3.connectionpool as cpool
from .stubs.requests_stubs import VCRVerifiedHTTPSConnection
@@ -44,6 +53,11 @@ def install(cassette):
cpool.VerifiedHTTPSConnection.cassette = cassette
cpool.HTTPConnection = VCRHTTPConnection
cpool.HTTPConnection.cassette = cassette
# patch requests v2.x
cpool.HTTPConnectionPool.ConnectionCls = VCRHTTPConnection
cpool.HTTPConnectionPool.cassette = cassette
cpool.HTTPSConnectionPool.ConnectionCls = VCRHTTPSConnection
cpool.HTTPSConnectionPool.cassette = cassette
except ImportError: # pragma: no cover
pass
@@ -58,16 +72,34 @@ def install(cassette):
except ImportError: # pragma: no cover
pass
# patch httplib2
try:
import httplib2 as cpool
from .stubs.httplib2_stubs import VCRHTTPConnectionWithTimeout
from .stubs.httplib2_stubs import VCRHTTPSConnectionWithTimeout
cpool.HTTPConnectionWithTimeout = VCRHTTPConnectionWithTimeout
cpool.HTTPSConnectionWithTimeout = VCRHTTPSConnectionWithTimeout
cpool.SCHEME_TO_CONNECTION = {
'http': VCRHTTPConnectionWithTimeout,
'https': VCRHTTPSConnectionWithTimeout
}
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
httplib.HTTPConnection = _HTTPConnection
httplib.HTTPSConnection = _HTTPSConnection
try:
import requests.packages.urllib3.connectionpool as cpool
# unpatch requests v1.x
cpool.VerifiedHTTPSConnection = _VerifiedHTTPSConnection
cpool.HTTPConnection = _HTTPConnection
cpool.HTTPConnection = _cpoolHTTPConnection
# unpatch requests v2.x
cpool.HTTPConnectionPool.ConnectionCls = _cpoolHTTPConnection
cpool.HTTPSConnection = _cpoolHTTPSConnection
cpool.HTTPSConnectionPool.ConnectionCls = _cpoolHTTPSConnection
except ImportError: # pragma: no cover
pass
@@ -75,5 +107,16 @@ def reset():
import urllib3.connectionpool as cpool
cpool.VerifiedHTTPSConnection = _VerifiedHTTPSConnection
cpool.HTTPConnection = _HTTPConnection
cpool.HTTPSConnection = _HTTPSConnection
cpool.HTTPConnectionPool.ConnectionCls = _HTTPConnection
cpool.HTTPSConnectionPool.ConnectionCls = _HTTPSConnection
except ImportError: # pragma: no cover
pass
try:
import httplib2 as cpool
cpool.HTTPConnectionWithTimeout = _HTTPConnectionWithTimeout
cpool.HTTPSConnectionWithTimeout = _HTTPSConnectionWithTimeout
cpool.SCHEME_TO_CONNECTION = _SCHEME_TO_CONNECTION
except ImportError: # pragma: no cover
pass

View File

@@ -3,21 +3,10 @@ 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

@@ -10,6 +10,11 @@ class Request(object):
# make headers a frozenset so it will be hashable
self.headers = frozenset(headers.items())
def add_header(self, key, value):
tmp = dict(self.headers)
tmp[key] = value
self.headers = frozenset(tmp.iteritems())
@property
def url(self):
return "{0}://{1}{2}".format(self.protocol, self.host, self.path)

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

@@ -0,0 +1,73 @@
import six
def convert_to_bytes(resp):
resp = convert_headers_to_bytes(resp)
resp = convert_body_to_bytes(resp)
return resp
def convert_to_unicode(resp):
resp = convert_headers_to_unicode(resp)
resp = convert_body_to_unicode(resp)
return resp
def convert_headers_to_bytes(resp):
try:
resp['headers'] = [h.encode('utf-8') for h in resp['headers']]
except (KeyError, TypeError):
pass
return resp
def convert_headers_to_unicode(resp):
try:
resp['headers'] = [h.decode('utf-8') for h in resp['headers']]
except (KeyError, TypeError):
pass
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,4 +1,5 @@
from vcr.request import Request
from . import compat
try:
import simplejson as json
except ImportError:
@@ -11,22 +12,17 @@ def _json_default(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]
responses = [compat.convert_to_bytes(r['response']) for r in data]
return requests, responses
def serialize(cassette_dict):
data = ([{
'request': request._to_dict(),
'response': response,
'response': compat.convert_to_unicode(response),
} for request, response in zip(
cassette_dict['requests'],
cassette_dict['responses']

View File

@@ -1,4 +1,6 @@
import sys
import yaml
from . import compat
# Use the libYAML versions if possible
try:
@@ -6,11 +8,40 @@ try:
except ImportError:
from yaml import Loader, Dumper
"""
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 _restore_frozenset():
"""
Restore __builtin__.frozenset for cassettes serialized in python2 but
deserialized in python3 and builtins.frozenset for cassettes serialized
in python3 and deserialized in python2
"""
if '__builtin__' not in sys.modules:
import builtins
sys.modules['__builtin__'] = builtins
if 'builtins' not in sys.modules:
sys.modules['builtins'] = sys.modules['__builtin__']
def deserialize(cassette_string):
_restore_frozenset()
data = yaml.load(cassette_string, Loader=Loader)
requests = [r['request'] for r in data]
responses = [r['response'] for r in data]
responses = [compat.convert_to_bytes(r['response']) for r in data]
return requests, responses
@@ -20,6 +51,6 @@ def serialize(cassette_dict):
'response': response,
} for request, response in zip(
cassette_dict['requests'],
cassette_dict['responses']
[compat.convert_to_unicode(r) for r in cassette_dict['responses']],
)])
return yaml.dump(data, Dumper=Dumper)

View File

@@ -1,63 +1,106 @@
'''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 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
class VCRHTTPResponse(object):
def parse_headers_backwards_compat(header_dict):
"""
In vcr 0.6.0, I changed the cassettes to store
headers as a list instead of a dict. This method
parses the old dictionary-style headers for
backwards-compatability reasons.
"""
msg = HTTPMessage(BytesIO(""))
for key, val in header_dict.items():
msg.addheader(key, val)
msg.headers.append("{0}:{1}".format(key, val))
return msg
def parse_headers(header_list):
if isinstance(header_list, dict):
return parse_headers_backwards_compat(header_list)
headers = b"".join(header_list) + b"\r\n"
return compat.get_httpmessage(headers)
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 compat.get_header_items(message)
def getheader(self, header, default=None):
headers = dict(((k, v) for k, v in self.getheaders()))
return headers.get(header, default)
class VCRConnectionMixin:
class VCRConnection:
# A reference to the cassette that's currently being patched in
cassette = None
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,
host=self.real_connection.host,
port=self.real_connection.port,
method=method,
path=url,
body=body,
@@ -69,6 +112,26 @@ class VCRConnectionMixin:
# allows me to compare the entire length of the response to see if it
# exists in the cassette.
def putrequest(self, method, url, *args, **kwargs):
"""
httplib gives you more than one way to do it. This is a way
to start building up a request. Usually followed by a bunch
of putheader() calls.
"""
self._vcr_request = Request(
protocol=self._protocol,
host=self.real_connection.host,
port=self.real_connection.port,
method=method,
path=url,
body="",
headers={}
)
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
@@ -77,96 +140,48 @@ class VCRConnectionMixin:
'''
self._vcr_request.body = (self._vcr_request.body or '') + data
def _send_request(self, method, url, body, headers):
def close(self):
# Note: the real connection will only close if it's open, so
# no need to check that here.
self.real_connection.close()
def endheaders(self, *args, **kwargs):
"""
Coppy+pasted from python stdlib 2.6 source because it
has a call to self.send() which I have overridden
#stdlibproblems #fml
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.
"""
header_names = dict.fromkeys([k.lower() for k in headers])
skips = {}
if 'host' in header_names:
skips['skip_host'] = 1
if 'accept-encoding' in header_names:
skips['skip_accept_encoding'] = 1
self.putrequest(method, url, **skips)
if body and ('content-length' not in header_names):
thelen = None
try:
thelen = str(len(body))
except TypeError, te:
# If this is a file-like object, try to
# fstat its file descriptor
import os
try:
thelen = str(os.fstat(body.fileno()).st_size)
except (AttributeError, OSError):
# Don't send a length if this failed
if self.debuglevel > 0:
print "Cannot stat!!"
if thelen is not None:
self.putheader('Content-Length', thelen)
for hdr, value in headers.iteritems():
self.putheader(hdr, value)
self.endheaders()
if body:
self._baseclass.send(self, body)
def _send_output(self, message_body=None):
"""
Copy-and-pasted from httplib, just so I can modify the self.send()
calls to call the superclass's send(), since I had to override the
send() behavior, since send() is both an external and internal
httplib API.
"""
self._buffer.extend(("", ""))
msg = "\r\n".join(self._buffer)
del self._buffer[:]
# If msg and message_body are sent in a single send() call,
# it will avoid performance problems caused by the interaction
# between delayed ack and the Nagle algorithm.
if isinstance(message_body, str):
msg += message_body
message_body = None
self._baseclass.send(self, msg)
if message_body is not None:
#message_body was not a string (i.e. it is a file) and
#we must run the risk of Nagle
self._baseclass.send(self, message_body)
pass
def getresponse(self, _=False):
'''Retrieve a the response'''
# Check to see if the cassette has a response for this request. If so,
# then return it
if self._vcr_request in self.cassette and \
self.cassette.record_mode != "all":
response = self.cassette.response_of(self._vcr_request)
# Alert the cassette to the fact that we've served another
# response for the provided requests
self.cassette.mark_played(self._vcr_request)
self.cassette.record_mode != "all" and \
self.cassette.rewound:
response = self.cassette.play_response(self._vcr_request)
return VCRHTTPResponse(response)
else:
if self.cassette.write_protected:
raise Exception("cassette is write protected")
raise CannotOverwriteExistingCassetteException(
"Can't overwrite existing cassette (%r) in "
"your current record mode (%r)."
% (self.cassette._path, self.cassette.record_mode)
)
# Otherwise, we should send the request, then get the response
# and return it.
# make the request
self._baseclass.request(
self,
self.real_connection.request(
method=self._vcr_request.method,
url=self._vcr_request.url,
url=self._vcr_request.path,
body=self._vcr_request.body,
headers=dict(self._vcr_request.headers or {})
)
# get the response
response = self._baseclass.getresponse(self)
response = self.real_connection.getresponse()
# put the response into the cassette
response = {
@@ -174,32 +189,54 @@ class VCRConnectionMixin:
'code': response.status,
'message': response.reason
},
'headers': dict(response.getheaders()),
'headers': compat.get_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._vcr_request in self.cassette and \
self.cassette.record_mode != "all" and \
self.cassette.rewound:
# 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)
def __init__(self, *args, **kwargs):
# 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 install, reset
reset()
self.real_connection = self._baseclass(*args, **kwargs)
install(self.cassette)
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)

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

@@ -0,0 +1,45 @@
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):
if six.PY3:
return dict(message._headers).items()
else:
return message.dict.items()
def get_headers(response):
if six.PY3:
header_list = response.msg._headers
return [b': '.join((k.encode('utf-8'), v.encode('utf-8'))) + b'\r\n'
for k, v in header_list]
else:
return response.msg.headers
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 not 'ca_certs' 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