mirror of
https://github.com/kevin1024/vcrpy.git
synced 2025-12-09 01:03:24 +00:00
Add support for configurable record modes
This support will let you select one of four different behaviors for VCR's cassettes. Closes #23
This commit is contained in:
54
README.md
54
README.md
@@ -52,6 +52,7 @@ import vcr
|
||||
my_vcr = vcr.VCR(
|
||||
serializer = 'json',
|
||||
cassette_library_dir = 'fixtures/cassettes',
|
||||
record_mode = 'once',
|
||||
)
|
||||
|
||||
with my_vcr.use_cassette('test.json'):
|
||||
@@ -61,12 +62,53 @@ 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.
|
||||
|
||||
## 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
|
||||
@@ -159,6 +201,16 @@ 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.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. 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.
|
||||
* 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
|
||||
|
||||
2
setup.py
2
setup.py
@@ -19,7 +19,7 @@ class PyTest(TestCommand):
|
||||
sys.exit(errno)
|
||||
|
||||
setup(name='vcrpy',
|
||||
version='0.2.1',
|
||||
version='0.3.0',
|
||||
description="A Python port of Ruby's VCR to make mocking HTTP easier",
|
||||
author='Kevin McCarthy',
|
||||
author_email='me@kevinmccarthy.org',
|
||||
|
||||
@@ -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
|
||||
|
||||
86
tests/integration/test_record_mode.py
Normal file
86
tests/integration/test_record_mode.py
Normal file
@@ -0,0 +1,86 @@
|
||||
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_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()
|
||||
@@ -14,6 +14,7 @@ from .serializers import yamlserializer
|
||||
|
||||
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 +22,13 @@ class Cassette(object):
|
||||
new_cassette._load()
|
||||
return new_cassette
|
||||
|
||||
def __init__(self, path, serializer=yamlserializer):
|
||||
def __init__(self, path, serializer=yamlserializer, record_mode='once'):
|
||||
self._path = path
|
||||
self._serializer = serializer
|
||||
self.data = OrderedDict()
|
||||
self.play_counts = Counter()
|
||||
self.dirty = False
|
||||
self.record_mode = record_mode
|
||||
|
||||
@property
|
||||
def play_count(self):
|
||||
@@ -40,6 +42,20 @@ class Cassette(object):
|
||||
def responses(self):
|
||||
return self.data.values()
|
||||
|
||||
@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
|
||||
|
||||
@@ -4,13 +4,17 @@ from .serializers import yamlserializer, jsonserializer
|
||||
|
||||
|
||||
class VCR(object):
|
||||
def __init__(self, serializer='yaml', cassette_library_dir=None):
|
||||
def __init__(self,
|
||||
serializer='yaml',
|
||||
cassette_library_dir=None,
|
||||
record_mode="once"):
|
||||
self.serializer = serializer
|
||||
self.cassette_library_dir = cassette_library_dir
|
||||
self.serializers = {
|
||||
'yaml': yamlserializer,
|
||||
'json': jsonserializer,
|
||||
}
|
||||
self.record_mode = record_mode
|
||||
|
||||
def _get_serializer(self, serializer_name):
|
||||
try:
|
||||
@@ -34,6 +38,7 @@ class VCR(object):
|
||||
|
||||
merged_config = {
|
||||
"serializer": self._get_serializer(serializer_name),
|
||||
"record_mode": kwargs.get('record_mode', self.record_mode),
|
||||
}
|
||||
|
||||
return Cassette.load(path, **merged_config)
|
||||
|
||||
@@ -142,13 +142,17 @@ class VCRConnectionMixin:
|
||||
'''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":
|
||||
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:
|
||||
if self.cassette.write_protected:
|
||||
raise Exception("cassette is write protected")
|
||||
|
||||
# Otherwise, we should send the request, then get the response
|
||||
# and return it.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user