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

Compare commits

...

20 Commits

Author SHA1 Message Date
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
21 changed files with 698 additions and 141 deletions

View File

@@ -1,7 +1,8 @@
language: python
before_install: openssl version
env:
- WITH_REQUESTS="True"
- WITH_REQUESTS="2.x"
- WITH_REQUESTS="1.x"
- WITH_REQUESTS="False"
python:
- 2.6
@@ -9,5 +10,6 @@ python:
- pypy
install:
- pip install PyYAML pytest --use-mirrors
- if [ $WITH_REQUESTS = "True" ] ; then pip install requests; fi
- if [ $WITH_REQUESTS = "1.x" ] ; then pip install requests==1.2.3; fi
- if [ $WITH_REQUESTS = "2.x" ] ; then pip install requests; fi
script: python setup.py test

128
README.md
View File

@@ -52,6 +52,8 @@ import vcr
my_vcr = vcr.VCR(
serializer = 'json',
cassette_library_dir = 'fixtures/cassettes',
record_mode = 'once',
match_on = ['url', 'method'],
)
with my_vcr.use_cassette('test.json'):
@@ -61,12 +63,75 @@ 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 `['url', method']`
which means that requests with both the same URL and method (ie POST or
GET) are considered identical.
This can be configured by changing the `match_on` setting.
The following options are available :
* method (for example, POST or GET)
* url (the full URL, including the protocol)
* host (the hostname of the server receiving the request)
* path (excluding the hostname)
* body (the entire request body)
* headers (the headers of the request)
If these options don't work for you, you can also register your own
request matcher. This is described in the Advanced section of this
README.
## Record Modes
VCR supports 4 record modes (with the same behavior as Ruby's VCR):
### once
* Replay previously recorded interactions.
* Record new interactions if there is no cassette file.
* Cause an error to be raised for new requests if there is a cassette file.
It is similar to the new_episodes record mode, but will prevent new,
unexpected requests from being made (i.e. because the request URI
changed).
once is the default record mode, used when you do not set one.
### new_episodes
* Record new interactions.
* Replay previously recorded interactions.
It is similar to the once record mode, but will always record new
interactions, even if you have an existing recorded one that is similar,
but not identical.
This was the default behavior in versions < 0.3.0
### none
* Replay previously recorded interactions.
* Cause an error to be raised for any new requests.
This is useful when your code makes potentially dangerous
HTTP requests. The none record mode guarantees that no
new HTTP requests will be made.
### all
* Record new interactions.
* Never replay previously recorded interactions.
This can be temporarily used to force VCR to re-record
a cassette (i.e. to ensure the responses are not out of date)
or can be used when you simply want to log all HTTP requests.
## Advanced Features
If you want, VCR.py can return information about the cassette it is
@@ -97,8 +162,6 @@ 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.
The Request object has the following properties
@@ -122,7 +185,7 @@ you would like. Create your own module or class instance with 2 methods:
Finally, register your class with VCR to use your
new serializer.
```
```python
import vcr
BogoSerializer(object):
@@ -146,6 +209,41 @@ with my_vcr.use_cassette('test.bogo'):
```
## Register your own request matcher
Create your own method with the following signature
```python
def my_matcher(r1, r2):
```
Your method receives the two requests and must return True if they
match, False if they don't.
Finally, register your method with VCR to use your
new request matcher.
```python
import vcr
def jurassic_matcher(r1, r2):
return r1.url == r2.url and 'JURASSIC PARK' in r1.body
my_vcr = vcr.VCR()
my_vcr.register_matcher('jurassic', jurassic_matcher)
with my_vcr.use_cassette('test.yml', match_on=['jurassic']):
# your http here
# After you register, you can set the default match_on to use your new matcher
my_vcr.match_on = ['jurassic']
with my_vcr.use_cassette('test.yml'):
# your http here
```
##Installation
VCR.py is a package on PyPI, so you can `pip install vcrpy` (first you may need to `brew install libyaml` [[Homebrew](http://mxcl.github.com/homebrew/)])
@@ -159,6 +257,28 @@ This library is a work in progress, so the API might change on you.
There are probably some [bugs](https://github.com/kevin1024/vcrpy/issues?labels=bug&page=1&state=open) floating around too.
##Changelog
* 0.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

View File

@@ -19,8 +19,8 @@ class PyTest(TestCommand):
sys.exit(errno)
setup(name='vcrpy',
version='0.2.1',
description="A Python port of Ruby's VCR to make mocking HTTP easier",
version='0.4.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',
@@ -39,10 +39,10 @@ setup(name='vcrpy',
},
install_requires=['PyYAML'],
license='MIT',
tests_require=['pytest'],
tests_require=['pytest','mock'],
cmdclass={'test': PyTest},
classifiers=[
'Development Status :: 3 - Alpha',
'Development Status :: 4 - Beta',
'Environment :: Console',
'Intended Audience :: Developers',
'Programming Language :: Python',

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)
except Exception:
assert False
assert True

View File

@@ -1,6 +1,7 @@
import os
import json
import urllib2
import pytest
import vcr
@@ -34,3 +35,24 @@ def test_override_set_cassette_library_dir(tmpdir):
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'))):
urllib2.urlopen('http://httpbin.org/')
with my_vcr.use_cassette(str(tmpdir.join('test.json'))) as cass:
urllib2.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

@@ -45,7 +45,7 @@ def test_disk_saver_write(tmpdir):
# the mtime doesn't change
time.sleep(1)
with vcr.use_cassette(fname) as cass:
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()
assert cass.play_count == 1

View File

@@ -0,0 +1,20 @@
import pytest
from urllib2 import urlopen
import vcr
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,108 @@
import os
import urllib2
import pytest
import vcr
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()
with vcr.use_cassette(testfile, record_mode="once") as cass:
# make the same request again
response = urllib2.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()
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 = urllib2.urlopen('http://httpbin.org/').read()
response2 = urllib2.urlopen('http://httpbin.org/').read()
with vcr.use_cassette(testfile, record_mode="once") as cass:
# do it again
response = urllib2.urlopen('http://httpbin.org/').read()
response = urllib2.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 = urllib2.urlopen('http://httpbin.org/').read()
response2 = urllib2.urlopen('http://httpbin.org/').read()
response2 = urllib2.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 = urllib2.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()
# in the "new_episodes" record mode, we can add more requests to
# a cassette without repurcussions.
response = urllib2.urlopen('http://httpbin.org/get').read()
# the first interaction was not re-recorded, but the second was added
assert cass.play_count == 1
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 = urllib2.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()
# in the "all" record mode, we can add more requests to
# a cassette without repurcussions.
response = urllib2.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 = urllib2.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 = urllib2.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()
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()

View File

@@ -0,0 +1,36 @@
import urllib2
import vcr
def true_matcher(r1, r2):
return True
def false_matcher(r1, r2):
return False
def test_registered_serializer_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
urllib2.urlopen('http://httpbin.org/')
urllib2.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
urllib2.urlopen('http://httpbin.org/get')
urllib2.urlopen('https://httpbin.org/get')
def test_registered_serializer_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
urllib2.urlopen('http://httpbin.org/')
urllib2.urlopen('https://httpbin.org/get')
assert len(cass) == 2

View File

@@ -24,7 +24,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

@@ -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,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'))) as 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('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'))) as cass:
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,19 @@ 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)

View File

@@ -25,52 +25,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 = urllib2.urlopen(url).getcode()
with vcr.use_cassette(str(tmpdir.join('atts.yaml'))) as cass:
assert code == urllib2.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 = urllib2.urlopen(url).read()
with vcr.use_cassette(str(tmpdir.join('body.yaml'))) as cass:
assert body == urllib2.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()
with vcr.use_cassette(str(tmpdir.join('headers.yaml'))) as cass:
open2 = urllib2.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
map(urllib2.urlopen, urls)
assert len(cass) == len(urls)
def test_get_data(scheme, tmpdir):
@@ -78,14 +70,12 @@ 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()
with vcr.use_cassette(str(tmpdir.join('get_data.yaml'))) as cass:
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
assert res1 == res2
def test_post_data(scheme, tmpdir):
@@ -93,13 +83,13 @@ def test_post_data(scheme, tmpdir):
data = urlencode({'some': 1, 'data': 'here'})
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()
with vcr.use_cassette(str(tmpdir.join('post_data.yaml'))) as cass:
res2 = urllib2.urlopen(url, data).read()
assert res1 == res2
# Ensure that we've now cached a single response
assert_cassette_has_one_response(cass)
assert res1 == res2
assert_cassette_has_one_response(cass)
def test_post_unicode_data(scheme, tmpdir):
@@ -107,13 +97,11 @@ def test_post_unicode_data(scheme, tmpdir):
data = urlencode({'snowman': u''.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()
with vcr.use_cassette(str(tmpdir.join('post_data.yaml'))) as cass:
res2 = urllib2.urlopen(url, data).read()
assert res1 == res2
# Ensure that we've now cached a single response
assert_cassette_has_one_response(cass)
assert res1 == res2
assert_cassette_has_one_response(cass)
def test_cross_scheme(tmpdir):

View File

@@ -3,6 +3,8 @@ requests = pytest.importorskip("requests")
import vcr
import httplib
def test_domain_redirect():
'''Ensure that redirects across domains are considered unique'''
@@ -15,3 +17,38 @@ 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.json') as cass:
_pretend_to_be_flickr_library()
assert len(cass) == 1
with vcr.use_cassette('fixtures/vcr_cassettes/flickr.json') 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

View File

@@ -1,5 +1,6 @@
import pytest
import yaml
import mock
from vcr.cassette import Cassette
@@ -17,21 +18,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,18 +32,25 @@ 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
@mock.patch('vcr.cassette.requests_match', _mock_requests_match)
def test_cassette_response_of():
a = Cassette('test')
a.append('foo', 'bar')
assert a.response_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):

23
tox.ini
View File

@@ -4,7 +4,7 @@
# and then run "tox" from this directory.
[tox]
envlist = py26, py27, pypy, py26requests, py27requests, pypyrequests
envlist = py26, py27, pypy, py26requests, py27requests, pypyrequests, py26oldrequests, py27oldrequests, pypyoldrequests
[testenv]
commands =
@@ -13,6 +13,27 @@ deps =
pytest
PyYAML
[testenv:py26oldrequests]
basepython = python2.6
deps =
pytest
PyYAML
requests==1.2.3
[testenv:py27oldrequests]
basepython = python2.7
deps =
pytest
PyYAML
requests==1.2.3
[testenv:pypyoldrequests]
basepython = pypy
deps =
pytest
PyYAML
requests==1.2.3
[testenv:py26requests]
basepython = python2.6
deps =

View File

@@ -10,10 +10,12 @@ except ImportError:
from .patch import install, reset
from .persist import load_cassette, save_cassette
from .serializers import yamlserializer
from .matchers import requests_match, url, method
class Cassette(object):
'''A container for recorded requests and responses'''
@classmethod
def load(cls, path, **kwargs):
'''Load in the cassette stored at the provided path'''
@@ -21,12 +23,21 @@ class Cassette(object):
new_cassette._load()
return new_cassette
def __init__(self, path, serializer=yamlserializer):
def __init__(self,
path,
serializer=yamlserializer,
record_mode='once',
match_on=[url, method]):
self._path = path
self._serializer = serializer
self.data = OrderedDict()
self._match_on = match_on
# 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):
@@ -34,26 +45,35 @@ class Cassette(object):
@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
self.data.append((request, response))
self.dirty = True
def response_of(self, request):
'''Find the response corresponding to a request'''
return self.data[request]
'''
Find the response corresponding to a request
'''
for index, (stored_request, response) in enumerate(self.data):
if requests_match(request, stored_request, self._match_on):
if self.play_counts[index] == 0:
self.play_counts[index] += 1
return response
# I decided that a KeyError is the best exception to raise
# if the cassette doesn't contain the request asked for.
raise KeyError
def _as_dict(self):
return {"requests": self.requests, "responses": self.responses}
@@ -76,6 +96,7 @@ class Cassette(object):
for request, response in zip(requests, responses):
self.append(request, response)
self.dirty = False
self.rewound = True
except IOError:
pass
@@ -90,7 +111,10 @@ class Cassette(object):
def __contains__(self, request):
'''Return whether or not a request has been stored'''
return request in self.data
for stored_request, response in self.data:
if requests_match(stored_request, request, self._match_on):
return True
return False
def __enter__(self):
'''Patch the fetching libraries we know about'''

View File

@@ -1,16 +1,32 @@
import os
from .cassette import Cassette
from .serializers import yamlserializer, jsonserializer
from .matchers import method, url, host, path, headers, body
class VCR(object):
def __init__(self, serializer='yaml', cassette_library_dir=None):
def __init__(self,
serializer='yaml',
cassette_library_dir=None,
record_mode="once",
match_on=['url', 'method'],
):
self.serializer = serializer
self.match_on = match_on
self.cassette_library_dir = cassette_library_dir
self.serializers = {
'yaml': yamlserializer,
'json': jsonserializer,
}
self.matchers = {
'method': method,
'url': url,
'host': host,
'path': path,
'headers': headers,
'body': body,
}
self.record_mode = record_mode
def _get_serializer(self, serializer_name):
try:
@@ -22,8 +38,18 @@ class VCR(object):
raise KeyError
return serializer
def _get_matchers(self, matcher_names):
try:
matchers = [self.matchers[m] for m in matcher_names]
except KeyError:
raise KeyError(
"Matcher {0} doesn't exist or isn't registered".format(m)
)
return matchers
def use_cassette(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
@@ -34,9 +60,14 @@ class VCR(object):
merged_config = {
"serializer": self._get_serializer(serializer_name),
"match_on": self._get_matchers(matcher_names),
"record_mode": kwargs.get('record_mode', self.record_mode),
}
return Cassette.load(path, **merged_config)
def register_serializer(self, name, serializer):
self.serializers[name] = serializer
def register_matcher(self, name, matcher):
self.matchers[name] = matcher

26
vcr/matchers.py Normal file
View File

@@ -0,0 +1,26 @@
def method(r1, r2):
return r1.method == r2.method
def url(r1, r2):
return r1.url == r2.url
def host(r1, r2):
return r1.host == r2.host
def path(r1, r2):
return r1.path == r2.path
def body(r1, r2):
return r1.body == r2.body
def headers(r1, r2):
return r1.headers == r2.headers
def requests_match(r1, r2, matchers):
return all(m(r1, r2) for m in matchers)

View File

@@ -25,14 +25,18 @@ except ImportError: # pragma: no cover
def install(cassette):
'''Install a cassette in lieu of actuall fetching'''
"""
Patch all the HTTPConnections references we can find!
This replaces the actual HTTPConnection with a VCRHTTPConnection
object which knows how to save to / read from cassettes
"""
httplib.HTTPConnection = httplib.HTTP._connection_class = VCRHTTPConnection
httplib.HTTPSConnection = httplib.HTTPS._connection_class = (
VCRHTTPSConnection)
httplib.HTTPConnection.cassette = cassette
httplib.HTTPSConnection.cassette = cassette
# patch requests
# patch requests v1.x
try:
import requests.packages.urllib3.connectionpool as cpool
from .stubs.requests_stubs import VCRVerifiedHTTPSConnection
@@ -40,6 +44,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
@@ -64,6 +73,8 @@ def reset():
import requests.packages.urllib3.connectionpool as cpool
cpool.VerifiedHTTPSConnection = _VerifiedHTTPSConnection
cpool.HTTPConnection = _HTTPConnection
cpool.HTTPConnectionPool.ConnectionCls = _HTTPConnection
cpool.HTTPSConnectionPool.ConnectionCls = _HTTPSConnection
except ImportError: # pragma: no cover
pass
@@ -71,5 +82,7 @@ def reset():
import urllib3.connectionpool as cpool
cpool.VerifiedHTTPSConnection = _VerifiedHTTPSConnection
cpool.HTTPConnection = _HTTPConnection
cpool.HTTPConnectionPool.ConnectionCls = _HTTPConnection
cpool.HTTPSConnectionPool.ConnectionCls = _HTTPSConnection
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

@@ -16,6 +16,7 @@ class VCRHTTPResponse(object):
self.status = recorded_response['status']['code']
self.version = None
self._content = StringIO(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
@@ -37,12 +38,13 @@ class VCRHTTPResponse(object):
return self._content.read(*args, **kwargs)
def close(self):
self.closed = True
return True
def isclosed(self):
# Urllib3 seems to call this because it actually uses
# the weird chunking support in httplib
return True
return self.closed
def getheaders(self):
return self.recorded_response['headers'].iteritems()
@@ -64,29 +66,127 @@ class VCRConnectionMixin:
headers=headers or {}
)
# 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 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):
self._restore_socket()
self._baseclass.close(self)
def _restore_socket(self):
"""
Since some libraries (REQUESTS!!) decide to set options on
connection.socket, I need to delete the socket attribute
(which makes requests think this is a AppEngine connection)
and then restore it when I want to make the actual request.
This function restores it to its standard initial value
(which is None)
"""
if not hasattr(self, 'sock'):
self.sock = None
def _send_request(self, method, url, body, headers):
"""
Copy+pasted from python stdlib 2.6 source because it
has a call to self.send() which I have overridden
#stdlibproblems #fml
"""
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._restore_socket()
self._baseclass.send(self, msg)
if message_body is not None:
#message_body was not a string (i.e. it is a file) and
#we must run the risk of Nagle
self._baseclass.send(self, message_body)
def getresponse(self, _=False):
'''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:
if self._vcr_request in self.cassette and self.cassette.record_mode != "all" and self.cassette.rewound:
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)
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:
raise Exception("cassette is write protected")
# Otherwise, we should send the request, then get the response
# and return it.
# restore sock's value to None, since we need a real socket
self._restore_socket()
#make the actual request
self._baseclass.request(
self,
method=self._vcr_request.method,
url=self._vcr_request.path,
body=self._vcr_request.body,
headers=dict(self._vcr_request.headers or {})
)
# get the response
response = self._baseclass.getresponse(self)
# put the response into the cassette
response = {
'status': {
'code': response.status,
@@ -107,6 +207,8 @@ class VCRHTTPConnection(VCRConnectionMixin, HTTPConnection):
def __init__(self, *args, **kwargs):
HTTPConnection.__init__(self, *args, **kwargs)
# see VCRConnectionMixin._restore_socket for the motivation here
del self.sock
class VCRHTTPSConnection(VCRConnectionMixin, HTTPSConnection):
@@ -121,3 +223,5 @@ class VCRHTTPSConnection(VCRConnectionMixin, HTTPSConnection):
HTTPConnection.__init__(self, *args, **kwargs)
self.key_file = kwargs.pop('key_file', None)
self.cert_file = kwargs.pop('cert_file', None)
# see VCRConnectionMixin._restore_socket for the motivation here
del self.sock