mirror of
https://github.com/kevin1024/vcrpy.git
synced 2025-12-09 09:13: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:
62
README.md
62
README.md
@@ -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
|
||||
|
||||
2
setup.py
2
setup.py
@@ -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',
|
||||
|
||||
32
tests/integration/test_register_matcher.py
Normal file
32
tests/integration/test_register_matcher.py
Normal 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
|
||||
@@ -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):
|
||||
|
||||
@@ -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'''
|
||||
|
||||
@@ -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
26
vcr/matchers.py
Normal 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)
|
||||
Reference in New Issue
Block a user