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

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.
This commit is contained in:
Kevin McCarthy
2013-09-16 20:09:25 -10:00
parent 03c22d79dd
commit a66f462dcd
7 changed files with 184 additions and 10 deletions

View File

@@ -53,6 +53,7 @@ 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'):
@@ -68,6 +69,28 @@ with vcr.use_cassette('test.yml', serializer='json', record_mode='once'):
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):
@@ -188,6 +211,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.
```
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/)])
@@ -203,7 +261,9 @@ There are probably some [bugs](https://github.com/kevin1024/vcrpy/issues?labels=
##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,
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

View File

@@ -39,7 +39,7 @@ setup(name='vcrpy',
},
install_requires=['PyYAML'],
license='MIT',
tests_require=['pytest'],
tests_require=['pytest','mock'],
cmdclass={'test': PyTest},
classifiers=[
'Development Status :: 3 - Alpha',

View File

@@ -0,0 +1,32 @@
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')
assert len(cass) == 1
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

@@ -1,5 +1,6 @@
import pytest
import yaml
import mock
from vcr.cassette import Cassette
@@ -46,18 +47,24 @@ 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):

View File

@@ -10,6 +10,7 @@ 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):
@@ -22,10 +23,17 @@ class Cassette(object):
new_cassette._load()
return new_cassette
def __init__(self, path, serializer=yamlserializer, record_mode='once'):
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.record_mode = record_mode
@@ -36,11 +44,11 @@ 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]
@property
def rewound(self):
@@ -64,12 +72,25 @@ class Cassette(object):
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
'''
responses = []
for stored_request, response in self.data:
if requests_match(request, stored_request, self._match_on):
responses.append(response)
index = self.play_counts[request]
try:
return responses[index]
except IndexError:
# I decided that a KeyError is the best exception to raise
# if the cassette doesn't contain the request asked for.
raise KeyError
def _as_dict(self):
return {"requests": self.requests, "responses": self.responses}
@@ -106,7 +127,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,6 +1,7 @@
import os
from .cassette import Cassette
from .serializers import yamlserializer, jsonserializer
from .matchers import method, url, host, path, headers, body
class VCR(object):
@@ -9,11 +10,20 @@ class VCR(object):
cassette_library_dir=None,
record_mode="once"):
self.serializer = serializer
self.match_on = ['url', 'method']
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):
@@ -26,8 +36,19 @@ 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:
print "Matcher {0} doesn't exist or isn't registered".format(
matcher_name
)
raise KeyError
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
@@ -38,6 +59,7 @@ 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),
}
@@ -45,3 +67,6 @@ class VCR(object):
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)