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(
|
my_vcr = vcr.VCR(
|
||||||
serializer = 'json',
|
serializer = 'json',
|
||||||
cassette_library_dir = 'fixtures/cassettes',
|
cassette_library_dir = 'fixtures/cassettes',
|
||||||
|
record_mode = 'once',
|
||||||
)
|
)
|
||||||
|
|
||||||
with my_vcr.use_cassette('test.json'):
|
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.
|
Otherwise, you can override options each time you use a cassette.
|
||||||
|
|
||||||
```python
|
```python
|
||||||
with vcr.use_cassette('test.yml', serializer='json'):
|
with vcr.use_cassette('test.yml', serializer='json', record_mode='once'):
|
||||||
# your http code here
|
# your http code here
|
||||||
```
|
```
|
||||||
|
|
||||||
Note: Per-cassette overrides take precedence over the global config.
|
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
|
## Advanced Features
|
||||||
|
|
||||||
If you want, VCR.py can return information about the cassette it is
|
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.
|
There are probably some [bugs](https://github.com/kevin1024/vcrpy/issues?labels=bug&page=1&state=open) floating around too.
|
||||||
|
|
||||||
##Changelog
|
##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.1: Fixed missing modules in setup.py
|
||||||
* 0.2.0: Added configuration API, which lets you configure some settings
|
* 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
|
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)
|
sys.exit(errno)
|
||||||
|
|
||||||
setup(name='vcrpy',
|
setup(name='vcrpy',
|
||||||
version='0.2.1',
|
version='0.3.0',
|
||||||
description="A Python port of Ruby's VCR to make mocking HTTP easier",
|
description="A Python port of Ruby's VCR to make mocking HTTP easier",
|
||||||
author='Kevin McCarthy',
|
author='Kevin McCarthy',
|
||||||
author_email='me@kevinmccarthy.org',
|
author_email='me@kevinmccarthy.org',
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ def test_disk_saver_write(tmpdir):
|
|||||||
# the mtime doesn't change
|
# the mtime doesn't change
|
||||||
time.sleep(1)
|
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://www.iana.org/domains/reserved').read()
|
||||||
urllib2.urlopen('http://httpbin.org/').read()
|
urllib2.urlopen('http://httpbin.org/').read()
|
||||||
assert cass.play_count == 1
|
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):
|
class Cassette(object):
|
||||||
'''A container for recorded requests and responses'''
|
'''A container for recorded requests and responses'''
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def load(cls, path, **kwargs):
|
def load(cls, path, **kwargs):
|
||||||
'''Load in the cassette stored at the provided path'''
|
'''Load in the cassette stored at the provided path'''
|
||||||
@@ -21,12 +22,13 @@ class Cassette(object):
|
|||||||
new_cassette._load()
|
new_cassette._load()
|
||||||
return new_cassette
|
return new_cassette
|
||||||
|
|
||||||
def __init__(self, path, serializer=yamlserializer):
|
def __init__(self, path, serializer=yamlserializer, record_mode='once'):
|
||||||
self._path = path
|
self._path = path
|
||||||
self._serializer = serializer
|
self._serializer = serializer
|
||||||
self.data = OrderedDict()
|
self.data = OrderedDict()
|
||||||
self.play_counts = Counter()
|
self.play_counts = Counter()
|
||||||
self.dirty = False
|
self.dirty = False
|
||||||
|
self.record_mode = record_mode
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def play_count(self):
|
def play_count(self):
|
||||||
@@ -40,6 +42,20 @@ class Cassette(object):
|
|||||||
def responses(self):
|
def responses(self):
|
||||||
return self.data.values()
|
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):
|
def mark_played(self, request):
|
||||||
'''
|
'''
|
||||||
Alert the cassette of a request that's been played
|
Alert the cassette of a request that's been played
|
||||||
|
|||||||
@@ -4,13 +4,17 @@ from .serializers import yamlserializer, jsonserializer
|
|||||||
|
|
||||||
|
|
||||||
class VCR(object):
|
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.serializer = serializer
|
||||||
self.cassette_library_dir = cassette_library_dir
|
self.cassette_library_dir = cassette_library_dir
|
||||||
self.serializers = {
|
self.serializers = {
|
||||||
'yaml': yamlserializer,
|
'yaml': yamlserializer,
|
||||||
'json': jsonserializer,
|
'json': jsonserializer,
|
||||||
}
|
}
|
||||||
|
self.record_mode = record_mode
|
||||||
|
|
||||||
def _get_serializer(self, serializer_name):
|
def _get_serializer(self, serializer_name):
|
||||||
try:
|
try:
|
||||||
@@ -34,6 +38,7 @@ class VCR(object):
|
|||||||
|
|
||||||
merged_config = {
|
merged_config = {
|
||||||
"serializer": self._get_serializer(serializer_name),
|
"serializer": self._get_serializer(serializer_name),
|
||||||
|
"record_mode": kwargs.get('record_mode', self.record_mode),
|
||||||
}
|
}
|
||||||
|
|
||||||
return Cassette.load(path, **merged_config)
|
return Cassette.load(path, **merged_config)
|
||||||
|
|||||||
@@ -142,13 +142,17 @@ 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:
|
if self._vcr_request in self.cassette and \
|
||||||
|
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
|
# Alert the cassette to the fact that we've served another
|
||||||
# response for the provided requests
|
# response for the provided requests
|
||||||
self.cassette.mark_played(self._vcr_request)
|
self.cassette.mark_played(self._vcr_request)
|
||||||
return VCRHTTPResponse(response)
|
return VCRHTTPResponse(response)
|
||||||
else:
|
else:
|
||||||
|
if self.cassette.write_protected:
|
||||||
|
raise Exception("cassette is write protected")
|
||||||
|
|
||||||
# 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.
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user