1
0
mirror of https://github.com/kevin1024/vcrpy.git synced 2025-12-09 01:03:24 +00:00

Compare commits

...

14 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
19 changed files with 252 additions and 145 deletions

View File

@@ -1,7 +1,8 @@
language: python language: python
before_install: openssl version before_install: openssl version
env: env:
- WITH_REQUESTS="True" - WITH_REQUESTS="2.x"
- WITH_REQUESTS="1.x"
- WITH_REQUESTS="False" - WITH_REQUESTS="False"
python: python:
- 2.6 - 2.6
@@ -9,5 +10,6 @@ python:
- pypy - pypy
install: install:
- pip install PyYAML pytest --use-mirrors - 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 script: python setup.py test

View File

@@ -100,7 +100,7 @@ VCR supports 4 record modes (with the same behavior as Ruby's VCR):
* Record new interactions if there is no cassette file. * Record new interactions if there is no cassette file.
* Cause an error to be raised for new requests if there is a cassette file. * Cause an error to be raised for new requests if there is a cassette file.
It is similar to the :new_episodes record mode, but will prevent new, It is similar to the new_episodes record mode, but will prevent new,
unexpected requests from being made (i.e. because the request URI unexpected requests from being made (i.e. because the request URI
changed). changed).
@@ -162,8 +162,6 @@ part of the API. The fields are as follows:
* `responses`: A list of the responses made. * `responses`: A list of the responses made.
* `play_count`: The number of times this cassette has had a response * `play_count`: The number of times this cassette has had a response
played back 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. * `response_of(request)`: Access the response for a given request.
The Request object has the following properties The Request object has the following properties
@@ -259,6 +257,15 @@ 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. There are probably some [bugs](https://github.com/kevin1024/vcrpy/issues?labels=bug&page=1&state=open) floating around too.
##Changelog ##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 * 0.3.0: *Backwards incompatible release* - Added support for record
modes, and changed the default recording behavior to the "once" record modes, and changed the default recording behavior to the "once" record
mode. Please see the documentation on record modes for more. Added mode. Please see the documentation on record modes for more. Added

View File

@@ -19,8 +19,8 @@ class PyTest(TestCommand):
sys.exit(errno) sys.exit(errno)
setup(name='vcrpy', setup(name='vcrpy',
version='0.3.0', version='0.4.0',
description="A Python port of Ruby's VCR to make mocking HTTP easier", description="Automatically mock your HTTP interactions to simplify and speed up testing",
author='Kevin McCarthy', author='Kevin McCarthy',
author_email='me@kevinmccarthy.org', author_email='me@kevinmccarthy.org',
url='https://github.com/kevin1024/vcrpy', url='https://github.com/kevin1024/vcrpy',

View File

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

View File

@@ -1,6 +1,7 @@
import os import os
import json import json
import urllib2 import urllib2
import pytest
import vcr 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 os.path.exists(str(tmpdir.join('subdir2').join('test.json')))
assert not os.path.exists(str(tmpdir.join('subdir').join('test.json'))) assert not os.path.exists(str(tmpdir.join('subdir').join('test.json')))
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

@@ -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

@@ -21,6 +21,28 @@ def test_once_record_mode(tmpdir):
response = urllib2.urlopen('http://httpbin.org/get').read() 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): def test_new_episodes_record_mode(tmpdir):
testfile = str(tmpdir.join('recordmode.yml')) testfile = str(tmpdir.join('recordmode.yml'))

View File

@@ -18,7 +18,11 @@ def test_registered_serializer_true_matcher(tmpdir):
# These 2 different urls are stored as the same request # These 2 different urls are stored as the same request
urllib2.urlopen('http://httpbin.org/') urllib2.urlopen('http://httpbin.org/')
urllib2.urlopen('https://httpbin.org/get') urllib2.urlopen('https://httpbin.org/get')
assert len(cass) == 1
with my_vcr.use_cassette(testfile, match_on=['true']) as cass:
# I can get the response twice even though I only asked for it once
urllib2.urlopen('http://httpbin.org/get')
urllib2.urlopen('https://httpbin.org/get')
def test_registered_serializer_false_matcher(tmpdir): def test_registered_serializer_false_matcher(tmpdir):

View File

@@ -24,7 +24,6 @@ def test_registered_serializer(tmpdir):
my_vcr.register_serializer('mock', ms) my_vcr.register_serializer('mock', ms)
tmpdir.join('test.mock').write('test_data') tmpdir.join('test.mock').write('test_data')
with my_vcr.use_cassette(str(tmpdir.join('test.mock')), serializer='mock'): with my_vcr.use_cassette(str(tmpdir.join('test.mock')), serializer='mock'):
urllib2.urlopen('http://httpbin.org/')
# Serializer deserialized once # Serializer deserialized once
assert ms.serialize_count == 1 assert ms.serialize_count == 1
# and serialized the test data string # and serialized the test data string

View File

@@ -5,7 +5,11 @@
import os import os
import pytest import pytest
import vcr import vcr
from assertions import assert_cassette_empty, assert_cassette_has_one_response from assertions import (
assert_cassette_empty,
assert_cassette_has_one_response,
assert_is_json
)
requests = pytest.importorskip("requests") requests = pytest.importorskip("requests")
@@ -21,33 +25,30 @@ def test_status_code(scheme, tmpdir):
'''Ensure that we can read the status code''' '''Ensure that we can read the status code'''
url = scheme + '://httpbin.org/' url = scheme + '://httpbin.org/'
with vcr.use_cassette(str(tmpdir.join('atts.yaml'))) as cass: with vcr.use_cassette(str(tmpdir.join('atts.yaml'))) as cass:
# Ensure that this is empty to begin with status_code = requests.get(url).status_code
assert_cassette_empty(cass)
assert requests.get(url).status_code == requests.get(url).status_code with vcr.use_cassette(str(tmpdir.join('atts.yaml'))) as cass:
# Ensure that we've now cached a single response assert status_code == requests.get(url).status_code
assert_cassette_has_one_response(cass)
def test_headers(scheme, tmpdir): def test_headers(scheme, tmpdir):
'''Ensure that we can read the headers back''' '''Ensure that we can read the headers back'''
url = scheme + '://httpbin.org/' url = scheme + '://httpbin.org/'
with vcr.use_cassette(str(tmpdir.join('headers.yaml'))) as cass: with vcr.use_cassette(str(tmpdir.join('headers.yaml'))) as cass:
# Ensure that this is empty to begin with headers = requests.get(url).headers
assert_cassette_empty(cass)
assert requests.get(url).headers == requests.get(url).headers with vcr.use_cassette(str(tmpdir.join('headers.yaml'))) as cass:
# Ensure that we've now cached a single response assert headers == requests.get(url).headers
assert_cassette_has_one_response(cass)
def test_body(tmpdir, scheme): def test_body(tmpdir, scheme):
'''Ensure the responses are all identical enough''' '''Ensure the responses are all identical enough'''
url = scheme + '://httpbin.org/bytes/1024' url = scheme + '://httpbin.org/bytes/1024'
with vcr.use_cassette(str(tmpdir.join('body.yaml'))) as cass: with vcr.use_cassette(str(tmpdir.join('body.yaml'))) as cass:
# Ensure that this is empty to begin with content = requests.get(url).content
assert_cassette_empty(cass)
assert requests.get(url).content == requests.get(url).content with vcr.use_cassette(str(tmpdir.join('body.yaml'))) as cass:
# Ensure that we've now cached a single response assert content == requests.get(url).content
assert_cassette_has_one_response(cass)
def test_auth(tmpdir, scheme): def test_auth(tmpdir, scheme):
@@ -55,14 +56,12 @@ def test_auth(tmpdir, scheme):
auth = ('user', 'passwd') auth = ('user', 'passwd')
url = scheme + '://httpbin.org/basic-auth/user/passwd' url = scheme + '://httpbin.org/basic-auth/user/passwd'
with vcr.use_cassette(str(tmpdir.join('auth.yaml'))) as cass: with vcr.use_cassette(str(tmpdir.join('auth.yaml'))) as cass:
# Ensure that this is empty to begin with
assert_cassette_empty(cass)
one = requests.get(url, auth=auth) one = requests.get(url, auth=auth)
with vcr.use_cassette(str(tmpdir.join('auth.yaml'))) as cass:
two = requests.get(url, auth=auth) two = requests.get(url, auth=auth)
assert one.content == two.content assert one.content == two.content
assert one.status_code == two.status_code assert one.status_code == two.status_code
# Ensure that we've now cached a single response
assert_cassette_has_one_response(cass)
def test_auth_failed(tmpdir, scheme): def test_auth_failed(tmpdir, scheme):
@@ -76,8 +75,6 @@ def test_auth_failed(tmpdir, scheme):
two = requests.get(url, auth=auth) two = requests.get(url, auth=auth)
assert one.content == two.content assert one.content == two.content
assert one.status_code == two.status_code == 401 assert one.status_code == two.status_code == 401
# Ensure that we've now cached a single response
assert_cassette_has_one_response(cass)
def test_post(tmpdir, scheme): def test_post(tmpdir, scheme):
@@ -85,22 +82,22 @@ def test_post(tmpdir, scheme):
data = {'key1': 'value1', 'key2': 'value2'} data = {'key1': 'value1', 'key2': 'value2'}
url = scheme + '://httpbin.org/post' url = scheme + '://httpbin.org/post'
with vcr.use_cassette(str(tmpdir.join('requests.yaml'))) as cass: with vcr.use_cassette(str(tmpdir.join('requests.yaml'))) as cass:
# Ensure that this is empty to begin with
assert_cassette_empty(cass)
req1 = requests.post(url, data).content req1 = requests.post(url, data).content
with vcr.use_cassette(str(tmpdir.join('requests.yaml'))) as cass:
req2 = requests.post(url, data).content req2 = requests.post(url, data).content
assert req1 == req2
# Ensure that we've now cached a single response assert req1 == req2
assert_cassette_has_one_response(cass)
def test_redirects(tmpdir, scheme): def test_redirects(tmpdir, scheme):
'''Ensure that we can handle redirects''' '''Ensure that we can handle redirects'''
url = scheme + '://httpbin.org/redirect-to?url=bytes/1024' url = scheme + '://httpbin.org/redirect-to?url=bytes/1024'
with vcr.use_cassette(str(tmpdir.join('requests.yaml'))) as cass: with vcr.use_cassette(str(tmpdir.join('requests.yaml'))) as cass:
# Ensure that this is empty to begin with content = requests.get(url).content
assert_cassette_empty(cass)
assert requests.get(url).content == requests.get(url).content with vcr.use_cassette(str(tmpdir.join('requests.yaml'))) as cass:
assert content == requests.get(url).content
# Ensure that we've now cached *two* responses. One for the redirect # Ensure that we've now cached *two* responses. One for the redirect
# and one for the final fetch # and one for the final fetch
assert len(cass) == 2 assert len(cass) == 2
@@ -117,3 +114,19 @@ def test_cross_scheme(tmpdir, scheme):
requests.get('http://httpbin.org/') requests.get('http://httpbin.org/')
assert cass.play_count == 0 assert cass.play_count == 0
assert len(cass) == 2 assert len(cass) == 2
def test_gzip(tmpdir, scheme):
'''
Ensure that requests (actually urllib3) is able to automatically decompress
the response body
'''
url = scheme + '://httpbin.org/gzip'
response = requests.get(url)
with vcr.use_cassette(str(tmpdir.join('gzip.yaml'))) 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''' '''Ensure we can read a response code from a fetch'''
url = scheme + '://httpbin.org/' url = scheme + '://httpbin.org/'
with vcr.use_cassette(str(tmpdir.join('atts.yaml'))) as cass: with vcr.use_cassette(str(tmpdir.join('atts.yaml'))) as cass:
# Ensure that this is empty to begin with code = urllib2.urlopen(url).getcode()
assert_cassette_empty(cass)
assert urllib2.urlopen(url).getcode() == urllib2.urlopen(url).getcode() with vcr.use_cassette(str(tmpdir.join('atts.yaml'))) as cass:
# Ensure that we've now cached a single response assert code == urllib2.urlopen(url).getcode()
assert_cassette_has_one_response(cass)
def test_random_body(scheme, tmpdir): def test_random_body(scheme, tmpdir):
'''Ensure we can read the content, and that it's served from cache''' '''Ensure we can read the content, and that it's served from cache'''
url = scheme + '://httpbin.org/bytes/1024' url = scheme + '://httpbin.org/bytes/1024'
with vcr.use_cassette(str(tmpdir.join('body.yaml'))) as cass: with vcr.use_cassette(str(tmpdir.join('body.yaml'))) as cass:
# Ensure that this is empty to begin with body = urllib2.urlopen(url).read()
assert_cassette_empty(cass)
assert urllib2.urlopen(url).read() == urllib2.urlopen(url).read() with vcr.use_cassette(str(tmpdir.join('body.yaml'))) as cass:
# Ensure that we've now cached a single response assert body == urllib2.urlopen(url).read()
assert_cassette_has_one_response(cass)
def test_response_headers(scheme, tmpdir): def test_response_headers(scheme, tmpdir):
'''Ensure we can get information from the response''' '''Ensure we can get information from the response'''
url = scheme + '://httpbin.org/' url = scheme + '://httpbin.org/'
with vcr.use_cassette(str(tmpdir.join('headers.yaml'))) as cass: with vcr.use_cassette(str(tmpdir.join('headers.yaml'))) as cass:
# Ensure that this is empty to begin with
assert_cassette_empty(cass)
open1 = urllib2.urlopen(url).info().items() open1 = urllib2.urlopen(url).info().items()
with vcr.use_cassette(str(tmpdir.join('headers.yaml'))) as cass:
open2 = urllib2.urlopen(url).info().items() open2 = urllib2.urlopen(url).info().items()
assert open1 == open2 assert open1 == open2
# Ensure that we've now cached a single response
assert_cassette_has_one_response(cass)
def test_multiple_requests(scheme, tmpdir): def test_multiple_requests(scheme, tmpdir):
'''Ensure that we can cache multiple requests''' '''Ensure that we can cache multiple requests'''
urls = [ urls = [
scheme + '://httpbin.org/',
scheme + '://httpbin.org/', scheme + '://httpbin.org/',
scheme + '://httpbin.org/get', scheme + '://httpbin.org/get',
scheme + '://httpbin.org/bytes/1024' scheme + '://httpbin.org/bytes/1024'
] ]
with vcr.use_cassette(str(tmpdir.join('multiple.yaml'))) as cass: with vcr.use_cassette(str(tmpdir.join('multiple.yaml'))) as cass:
for index in range(len(urls)): map(urllib2.urlopen, urls)
url = urls[index] assert len(cass) == len(urls)
assert len(cass) == index
assert cass.play_count == index
assert urllib2.urlopen(url).read() == urllib2.urlopen(url).read()
assert len(cass) == index + 1
assert cass.play_count == index + 1
def test_get_data(scheme, tmpdir): def test_get_data(scheme, tmpdir):
@@ -78,14 +70,12 @@ def test_get_data(scheme, tmpdir):
data = urlencode({'some': 1, 'data': 'here'}) data = urlencode({'some': 1, 'data': 'here'})
url = scheme + '://httpbin.org/get?' + data url = scheme + '://httpbin.org/get?' + data
with vcr.use_cassette(str(tmpdir.join('get_data.yaml'))) as cass: with vcr.use_cassette(str(tmpdir.join('get_data.yaml'))) as cass:
# Ensure that this is empty to begin with
assert_cassette_empty(cass)
res1 = urllib2.urlopen(url).read() res1 = urllib2.urlopen(url).read()
with vcr.use_cassette(str(tmpdir.join('get_data.yaml'))) as cass:
res2 = urllib2.urlopen(url).read() res2 = urllib2.urlopen(url).read()
assert res1 == res2
# Ensure that we've now cached a single response assert res1 == res2
assert len(cass) == 1
assert cass.play_count == 1
def test_post_data(scheme, tmpdir): def test_post_data(scheme, tmpdir):
@@ -93,13 +83,13 @@ def test_post_data(scheme, tmpdir):
data = urlencode({'some': 1, 'data': 'here'}) data = urlencode({'some': 1, 'data': 'here'})
url = scheme + '://httpbin.org/post' url = scheme + '://httpbin.org/post'
with vcr.use_cassette(str(tmpdir.join('post_data.yaml'))) as cass: with vcr.use_cassette(str(tmpdir.join('post_data.yaml'))) as cass:
# Ensure that this is empty to begin with
assert_cassette_empty(cass)
res1 = urllib2.urlopen(url, data).read() res1 = urllib2.urlopen(url, data).read()
with vcr.use_cassette(str(tmpdir.join('post_data.yaml'))) as cass:
res2 = urllib2.urlopen(url, data).read() res2 = urllib2.urlopen(url, data).read()
assert res1 == res2
# Ensure that we've now cached a single response assert res1 == res2
assert_cassette_has_one_response(cass) assert_cassette_has_one_response(cass)
def test_post_unicode_data(scheme, tmpdir): def test_post_unicode_data(scheme, tmpdir):
@@ -107,13 +97,11 @@ def test_post_unicode_data(scheme, tmpdir):
data = urlencode({'snowman': u''.encode('utf-8')}) data = urlencode({'snowman': u''.encode('utf-8')})
url = scheme + '://httpbin.org/post' url = scheme + '://httpbin.org/post'
with vcr.use_cassette(str(tmpdir.join('post_data.yaml'))) as cass: with vcr.use_cassette(str(tmpdir.join('post_data.yaml'))) as cass:
# Ensure that this is empty to begin with
assert_cassette_empty(cass)
res1 = urllib2.urlopen(url, data).read() res1 = urllib2.urlopen(url, data).read()
with vcr.use_cassette(str(tmpdir.join('post_data.yaml'))) as cass:
res2 = urllib2.urlopen(url, data).read() res2 = urllib2.urlopen(url, data).read()
assert res1 == res2 assert res1 == res2
# Ensure that we've now cached a single response assert_cassette_has_one_response(cass)
assert_cassette_has_one_response(cass)
def test_cross_scheme(tmpdir): def test_cross_scheme(tmpdir):

View File

@@ -45,3 +45,10 @@ def test_flickr_multipart_upload():
assert len(cass) == 1 assert len(cass) == 1
_pretend_to_be_flickr_library() _pretend_to_be_flickr_library()
assert cass.play_count == 1 assert cass.play_count == 1
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

@@ -18,21 +18,6 @@ def test_cassette_not_played():
assert not a.play_count assert not a.play_count
def test_cassette_played():
a = Cassette('test')
a.mark_played('foo')
a.mark_played('foo')
assert a.play_count == 2
def test_cassette_play_counter():
a = Cassette('test')
a.mark_played('foo')
a.mark_played('bar')
assert a.play_counts['foo'] == 1
assert a.play_counts['bar'] == 1
def test_cassette_append(): def test_cassette_append():
a = Cassette('test') a = Cassette('test')
a.append('foo', 'bar') a.append('foo', 'bar')
@@ -50,6 +35,7 @@ def test_cassette_len():
def _mock_requests_match(request1, request2, matchers): def _mock_requests_match(request1, request2, matchers):
return request1 == request2 return request1 == request2
@mock.patch('vcr.cassette.requests_match', _mock_requests_match) @mock.patch('vcr.cassette.requests_match', _mock_requests_match)
def test_cassette_contains(): def test_cassette_contains():
a = Cassette('test') a = Cassette('test')

23
tox.ini
View File

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

View File

@@ -36,6 +36,7 @@ class Cassette(object):
self.data = [] self.data = []
self.play_counts = Counter() self.play_counts = Counter()
self.dirty = False self.dirty = False
self.rewound = False
self.record_mode = record_mode self.record_mode = record_mode
@property @property
@@ -50,26 +51,11 @@ class Cassette(object):
def responses(self): def responses(self):
return [response for (request, response) in self.data] return [response for (request, response) in self.data]
@property
def rewound(self):
"""
If the cassette has already been recorded in another session, and has
been loaded again fresh from disk, it has been "rewound". This means
that it should be write-only, depending on the record mode specified
"""
return not self.dirty and self.play_count
@property @property
def write_protected(self): def write_protected(self):
return self.rewound and self.record_mode == 'once' or \ return self.rewound and self.record_mode == 'once' or \
self.record_mode == 'none' self.record_mode == 'none'
def mark_played(self, request):
'''
Alert the cassette of a request that's been played
'''
self.play_counts[request] += 1
def append(self, request, response): def append(self, request, response):
'''Add a request, response pair to this cassette''' '''Add a request, response pair to this cassette'''
self.data.append((request, response)) self.data.append((request, response))
@@ -80,17 +66,14 @@ class Cassette(object):
Find the response corresponding to a request Find the response corresponding to a request
''' '''
responses = [] for index, (stored_request, response) in enumerate(self.data):
for stored_request, response in self.data:
if requests_match(request, stored_request, self._match_on): if requests_match(request, stored_request, self._match_on):
responses.append(response) if self.play_counts[index] == 0:
index = self.play_counts[request] self.play_counts[index] += 1
try: return response
return responses[index] # I decided that a KeyError is the best exception to raise
except IndexError: # if the cassette doesn't contain the request asked for.
# I decided that a KeyError is the best exception to raise raise KeyError
# if the cassette doesn't contain the request asked for.
raise KeyError
def _as_dict(self): def _as_dict(self):
return {"requests": self.requests, "responses": self.responses} return {"requests": self.requests, "responses": self.responses}
@@ -113,6 +96,7 @@ class Cassette(object):
for request, response in zip(requests, responses): for request, response in zip(requests, responses):
self.append(request, response) self.append(request, response)
self.dirty = False self.dirty = False
self.rewound = True
except IOError: except IOError:
pass pass

View File

@@ -8,9 +8,11 @@ class VCR(object):
def __init__(self, def __init__(self,
serializer='yaml', serializer='yaml',
cassette_library_dir=None, cassette_library_dir=None,
record_mode="once"): record_mode="once",
match_on=['url', 'method'],
):
self.serializer = serializer self.serializer = serializer
self.match_on = ['url', 'method'] self.match_on = match_on
self.cassette_library_dir = cassette_library_dir self.cassette_library_dir = cassette_library_dir
self.serializers = { self.serializers = {
'yaml': yamlserializer, 'yaml': yamlserializer,
@@ -40,10 +42,9 @@ class VCR(object):
try: try:
matchers = [self.matchers[m] for m in matcher_names] matchers = [self.matchers[m] for m in matcher_names]
except KeyError: except KeyError:
print "Matcher {0} doesn't exist or isn't registered".format( raise KeyError(
matcher_name "Matcher {0} doesn't exist or isn't registered".format(m)
) )
raise KeyError
return matchers return matchers
def use_cassette(self, path, **kwargs): def use_cassette(self, path, **kwargs):

View File

@@ -36,7 +36,7 @@ def install(cassette):
httplib.HTTPConnection.cassette = cassette httplib.HTTPConnection.cassette = cassette
httplib.HTTPSConnection.cassette = cassette httplib.HTTPSConnection.cassette = cassette
# patch requests # patch requests v1.x
try: try:
import requests.packages.urllib3.connectionpool as cpool import requests.packages.urllib3.connectionpool as cpool
from .stubs.requests_stubs import VCRVerifiedHTTPSConnection from .stubs.requests_stubs import VCRVerifiedHTTPSConnection
@@ -44,6 +44,11 @@ def install(cassette):
cpool.VerifiedHTTPSConnection.cassette = cassette cpool.VerifiedHTTPSConnection.cassette = cassette
cpool.HTTPConnection = VCRHTTPConnection cpool.HTTPConnection = VCRHTTPConnection
cpool.HTTPConnection.cassette = cassette 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 except ImportError: # pragma: no cover
pass pass
@@ -68,6 +73,8 @@ def reset():
import requests.packages.urllib3.connectionpool as cpool import requests.packages.urllib3.connectionpool as cpool
cpool.VerifiedHTTPSConnection = _VerifiedHTTPSConnection cpool.VerifiedHTTPSConnection = _VerifiedHTTPSConnection
cpool.HTTPConnection = _HTTPConnection cpool.HTTPConnection = _HTTPConnection
cpool.HTTPConnectionPool.ConnectionCls = _HTTPConnection
cpool.HTTPSConnectionPool.ConnectionCls = _HTTPSConnection
except ImportError: # pragma: no cover except ImportError: # pragma: no cover
pass pass
@@ -75,5 +82,7 @@ def reset():
import urllib3.connectionpool as cpool import urllib3.connectionpool as cpool
cpool.VerifiedHTTPSConnection = _VerifiedHTTPSConnection cpool.VerifiedHTTPSConnection = _VerifiedHTTPSConnection
cpool.HTTPConnection = _HTTPConnection cpool.HTTPConnection = _HTTPConnection
cpool.HTTPConnectionPool.ConnectionCls = _HTTPConnection
cpool.HTTPSConnectionPool.ConnectionCls = _HTTPSConnection
except ImportError: # pragma: no cover except ImportError: # pragma: no cover
pass pass

View File

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

View File

@@ -16,6 +16,7 @@ class VCRHTTPResponse(object):
self.status = recorded_response['status']['code'] self.status = recorded_response['status']['code']
self.version = None self.version = None
self._content = StringIO(self.recorded_response['body']['string']) self._content = StringIO(self.recorded_response['body']['string'])
self.closed = False
# We are skipping the header parsing (they have already been parsed # We are skipping the header parsing (they have already been parsed
# at this point) and directly adding the headers to the header # at this point) and directly adding the headers to the header
@@ -37,12 +38,13 @@ class VCRHTTPResponse(object):
return self._content.read(*args, **kwargs) return self._content.read(*args, **kwargs)
def close(self): def close(self):
self.closed = True
return True return True
def isclosed(self): def isclosed(self):
# Urllib3 seems to call this because it actually uses # Urllib3 seems to call this because it actually uses
# the weird chunking support in httplib # the weird chunking support in httplib
return True return self.closed
def getheaders(self): def getheaders(self):
return self.recorded_response['headers'].iteritems() return self.recorded_response['headers'].iteritems()
@@ -77,9 +79,25 @@ class VCRConnectionMixin:
''' '''
self._vcr_request.body = (self._vcr_request.body or '') + data 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): def _send_request(self, method, url, body, headers):
""" """
Coppy+pasted from python stdlib 2.6 source because it Copy+pasted from python stdlib 2.6 source because it
has a call to self.send() which I have overridden has a call to self.send() which I have overridden
#stdlibproblems #fml #stdlibproblems #fml
""" """
@@ -132,6 +150,7 @@ class VCRConnectionMixin:
if isinstance(message_body, str): if isinstance(message_body, str):
msg += message_body msg += message_body
message_body = None message_body = None
self._restore_socket()
self._baseclass.send(self, msg) self._baseclass.send(self, msg)
if message_body is not None: if message_body is not None:
#message_body was not a string (i.e. it is a file) and #message_body was not a string (i.e. it is a file) and
@@ -142,12 +161,8 @@ class VCRConnectionMixin:
'''Retrieve a the response''' '''Retrieve a the response'''
# Check to see if the cassette has a response for this request. If so, # Check to see if the cassette has a response for this request. If so,
# then return it # then return it
if self._vcr_request in self.cassette and \ if self._vcr_request in self.cassette and self.cassette.record_mode != "all" and self.cassette.rewound:
self.cassette.record_mode != "all":
response = self.cassette.response_of(self._vcr_request) 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) return VCRHTTPResponse(response)
else: else:
if self.cassette.write_protected: if self.cassette.write_protected:
@@ -156,11 +171,14 @@ class VCRConnectionMixin:
# Otherwise, we should send the request, then get the response # Otherwise, we should send the request, then get the response
# and return it. # and return it.
# make the request # restore sock's value to None, since we need a real socket
self._restore_socket()
#make the actual request
self._baseclass.request( self._baseclass.request(
self, self,
method=self._vcr_request.method, method=self._vcr_request.method,
url=self._vcr_request.url, url=self._vcr_request.path,
body=self._vcr_request.body, body=self._vcr_request.body,
headers=dict(self._vcr_request.headers or {}) headers=dict(self._vcr_request.headers or {})
) )
@@ -189,6 +207,8 @@ class VCRHTTPConnection(VCRConnectionMixin, HTTPConnection):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
HTTPConnection.__init__(self, *args, **kwargs) HTTPConnection.__init__(self, *args, **kwargs)
# see VCRConnectionMixin._restore_socket for the motivation here
del self.sock
class VCRHTTPSConnection(VCRConnectionMixin, HTTPSConnection): class VCRHTTPSConnection(VCRConnectionMixin, HTTPSConnection):
@@ -203,3 +223,5 @@ class VCRHTTPSConnection(VCRConnectionMixin, HTTPSConnection):
HTTPConnection.__init__(self, *args, **kwargs) HTTPConnection.__init__(self, *args, **kwargs)
self.key_file = kwargs.pop('key_file', None) self.key_file = kwargs.pop('key_file', None)
self.cert_file = kwargs.pop('cert_file', None) self.cert_file = kwargs.pop('cert_file', None)
# see VCRConnectionMixin._restore_socket for the motivation here
del self.sock