mirror of
https://github.com/kevin1024/vcrpy.git
synced 2025-12-08 16:53:23 +00:00
Compare commits
136 Commits
v1.4.0
...
better_log
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f821fed418 | ||
|
|
50246791e3 | ||
|
|
8a5bf23d34 | ||
|
|
f6b8e4f8e7 | ||
|
|
2ac3fa9abe | ||
|
|
1324feae99 | ||
|
|
7990c549d1 | ||
|
|
327797c4ff | ||
|
|
ac510097e0 | ||
|
|
00d973a0f5 | ||
|
|
79ff59feae | ||
|
|
34252bc234 | ||
|
|
5f78657c52 | ||
|
|
00b4e451fe | ||
|
|
44564ba39f | ||
|
|
7f02a7e999 | ||
|
|
c28adea66d | ||
|
|
3f006cc261 | ||
|
|
0eda8ba482 | ||
|
|
d620095c36 | ||
|
|
c8180326ad | ||
|
|
d55d593d1c | ||
|
|
04f4a7fd2f | ||
|
|
6fd04f3675 | ||
|
|
420f83b6b1 | ||
|
|
c6adcc83b3 | ||
|
|
dc61f5f520 | ||
|
|
4450cb992f | ||
|
|
083b1ec686 | ||
|
|
97c924d8dd | ||
|
|
65398131a4 | ||
|
|
7312229aef | ||
|
|
b62265c0ad | ||
|
|
d00c60a4ad | ||
|
|
4ddfb47c9c | ||
|
|
f0b7c3f1e0 | ||
|
|
646d12df94 | ||
|
|
eda64bc3be | ||
|
|
efe6744eda | ||
|
|
58f4b98f7f | ||
|
|
3305f0ca7d | ||
|
|
7f02d65dd9 | ||
|
|
3e5553c56a | ||
|
|
a569dd4dc8 | ||
|
|
eb1cdad03a | ||
|
|
08bb3bd187 | ||
|
|
ae5580c8f9 | ||
|
|
f342f92f03 | ||
|
|
be3bf39161 | ||
|
|
29d37e410a | ||
|
|
8b7e6c0ab8 | ||
|
|
bd7c6ed03f | ||
|
|
1e414826e7 | ||
|
|
1e1c093b3c | ||
|
|
bb8f563135 | ||
|
|
ca3200d96e | ||
|
|
04b5978adc | ||
|
|
01f1f9fdc1 | ||
|
|
a82e8628c2 | ||
|
|
7d68f0577a | ||
|
|
d0aa5fddb7 | ||
|
|
e54aeadc68 | ||
|
|
c4a33d1cff | ||
|
|
8b59d73f25 | ||
|
|
eb394b90d9 | ||
|
|
14931dd47a | ||
|
|
89cdda86d1 | ||
|
|
ad48d71897 | ||
|
|
946ce17a97 | ||
|
|
4d438dac75 | ||
|
|
a234ad6b12 | ||
|
|
1d000ac652 | ||
|
|
21c176ee1e | ||
|
|
4fb5bef8e1 | ||
|
|
9717596e2c | ||
|
|
1660cc3a9f | ||
|
|
4beb023204 | ||
|
|
72eb5345d6 | ||
|
|
fe7d193d1a | ||
|
|
09b7ccf561 | ||
|
|
a4a80b431b | ||
|
|
025a3b422d | ||
|
|
bb05b2fcf7 | ||
|
|
f77ef81877 | ||
|
|
80ece7750f | ||
|
|
8a86d75dc5 | ||
|
|
33a4fb98c6 | ||
|
|
a046697567 | ||
|
|
c0286dfd97 | ||
|
|
cc9af1d5fb | ||
|
|
5f8407a8a1 | ||
|
|
c789c82c1d | ||
|
|
16b5b77bcd | ||
|
|
0a093786ed | ||
|
|
3986caf182 | ||
|
|
cc6c26646c | ||
|
|
3846a4ccef | ||
|
|
aae4ae255b | ||
|
|
92303a911a | ||
|
|
57e0e6c753 | ||
|
|
c37d607b97 | ||
|
|
7922fec9fe | ||
|
|
7d175b0f91 | ||
|
|
41949f7dc6 | ||
|
|
d14888ccd8 | ||
|
|
a9ede54064 | ||
|
|
ce7ceb0a1e | ||
|
|
e742d32a8a | ||
|
|
ccc1ccaa0e | ||
|
|
731a33a79a | ||
|
|
6cbc0fb279 | ||
|
|
789f118c98 | ||
|
|
4f07cb5257 | ||
|
|
ad6f635ac2 | ||
|
|
142b840eee | ||
|
|
32c687522d | ||
|
|
5fc33c7e70 | ||
|
|
0f81f023c8 | ||
|
|
e324a9677d | ||
|
|
28640beb7d | ||
|
|
c338d5d32c | ||
|
|
59aa351ca8 | ||
|
|
2323b9da5f | ||
|
|
0bbbc694b0 | ||
|
|
d293020617 | ||
|
|
daac863f0b | ||
|
|
5cfb005b48 | ||
|
|
4ade547779 | ||
|
|
dc8eedf555 | ||
|
|
5b9b6cd8b5 | ||
|
|
856c38479a | ||
|
|
52496cd091 | ||
|
|
bc26ce877a | ||
|
|
8db0d245a5 | ||
|
|
47544b08fe | ||
|
|
4e560fc8db |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -7,4 +7,5 @@ dist/
|
||||
*.egg-info/
|
||||
pytestdebug.log
|
||||
|
||||
fixtures/
|
||||
fixtures/
|
||||
/docs/_build
|
||||
|
||||
10
.travis.yml
10
.travis.yml
@@ -1,4 +1,5 @@
|
||||
language: python
|
||||
sudo: false
|
||||
before_install: openssl version
|
||||
env:
|
||||
global:
|
||||
@@ -9,12 +10,15 @@ env:
|
||||
- WITH_LIB="requests2.3"
|
||||
- WITH_LIB="requests2.4"
|
||||
- WITH_LIB="requests2.5"
|
||||
- WITH_LIB="requests2.6"
|
||||
- WITH_LIB="requests2.7"
|
||||
- WITH_LIB="requests1.x"
|
||||
- WITH_LIB="httplib2"
|
||||
- WITH_LIB="boto"
|
||||
- WITH_LIB="urllib31.7"
|
||||
- WITH_LIB="urllib31.9"
|
||||
- WITH_LIB="urllib31.10"
|
||||
- WITH_LIB="tornado"
|
||||
matrix:
|
||||
allow_failures:
|
||||
- env: WITH_LIB="boto"
|
||||
@@ -32,15 +36,19 @@ python:
|
||||
- 3.4
|
||||
- pypy
|
||||
install:
|
||||
- pip install PyYAML pytest pytest-localserver --use-mirrors
|
||||
- pip install .
|
||||
- if [ $WITH_LIB = "requests1.x" ] ; then pip install requests==1.2.3; fi
|
||||
- if [ $WITH_LIB = "requests2.2" ] ; then pip install requests==2.2.1; fi
|
||||
- if [ $WITH_LIB = "requests2.3" ] ; then pip install requests==2.3.0; fi
|
||||
- if [ $WITH_LIB = "requests2.4" ] ; then pip install requests==2.4.0; fi
|
||||
- if [ $WITH_LIB = "requests2.5" ] ; then pip install requests==2.5.0; fi
|
||||
- if [ $WITH_LIB = "requests2.6" ] ; then pip install requests==2.6.0; fi
|
||||
- if [ $WITH_LIB = "requests2.7" ] ; then pip install requests==2.7.0; fi
|
||||
- if [ $WITH_LIB = "httplib2" ] ; then pip install httplib2; fi
|
||||
- if [ $WITH_LIB = "boto" ] ; then pip install boto; fi
|
||||
- if [ $WITH_LIB = "urllib31.7" ] ; then pip install certifi urllib3==1.7.1; fi
|
||||
- if [ $WITH_LIB = "urllib31.9" ] ; then pip install certifi urllib3==1.9.1; fi
|
||||
- if [ $WITH_LIB = "urllib31.10" ] ; then pip install certifi urllib3==1.10.2; fi
|
||||
- if [ $WITH_LIB = "tornado" ] ; then pip install tornado==4.2 pytest-tornado; fi
|
||||
- if [ $WITH_LIB = "tornado" -a $TRAVIS_PYTHON_VERSION != "pypy" ] ; then pip install pycurl; fi
|
||||
script: python setup.py test
|
||||
|
||||
4
MANIFEST.in
Normal file
4
MANIFEST.in
Normal file
@@ -0,0 +1,4 @@
|
||||
include README.rst
|
||||
include LICENSE.txt
|
||||
include tox.ini
|
||||
recursive-include tests *
|
||||
594
README.md
594
README.md
@@ -1,594 +0,0 @@
|
||||
# VCR.py
|
||||
|
||||

|
||||
|
||||
This is a Python version of [Ruby's VCR library](https://github.com/vcr/vcr).
|
||||
|
||||
[](http://travis-ci.org/kevin1024/vcrpy)
|
||||
[](https://waffle.io/kevin1024/vcrpy)
|
||||
|
||||
## What it does
|
||||
Simplify and speed up testing HTTP by recording all HTTP interactions and
|
||||
saving them to "cassette" files, which are yaml files containing the contents
|
||||
of your requests and responses. Then when you run your tests again, they all
|
||||
just hit the text files instead of the internet. This speeds up your tests and
|
||||
lets you work offline.
|
||||
|
||||
If the server you are testing against ever changes its API, all you need to do
|
||||
is delete your existing cassette files, and run your tests again. All of the
|
||||
mocked responses will be updated with the new API.
|
||||
|
||||
## Compatibility Notes
|
||||
VCR.py supports Python 2.6 and 2.7, 3.3, 3.4, and [pypy](http://pypy.org).
|
||||
|
||||
The following http libraries are supported:
|
||||
|
||||
* urllib2
|
||||
* urllib3
|
||||
* http.client (python3)
|
||||
* requests (both 1.x and 2.x versions)
|
||||
* httplib2
|
||||
* boto
|
||||
|
||||
## Usage
|
||||
```python
|
||||
import vcr
|
||||
import urllib2
|
||||
|
||||
with vcr.use_cassette('fixtures/vcr_cassettes/synopsis.yaml'):
|
||||
response = urllib2.urlopen('http://www.iana.org/domains/reserved').read()
|
||||
assert 'Example domains' in response
|
||||
```
|
||||
|
||||
Run this test once, and VCR.py will record the HTTP request to
|
||||
`fixtures/vcr_cassettes/synopsis.yml`. Run it again, and VCR.py will replay the
|
||||
response from iana.org when the http request is made. This test is now fast (no
|
||||
real HTTP requests are made anymore), deterministic (the test will continue to
|
||||
pass, even if you are offline, or iana.org goes down for maintenance) and
|
||||
accurate (the response will contain the same headers and body you get from a
|
||||
real request).
|
||||
|
||||
You can also use VCR.py as a decorator. The same request above would look like
|
||||
this:
|
||||
|
||||
```python
|
||||
@vcr.use_cassette('fixtures/vcr_cassettes/synopsis.yaml')
|
||||
def test_iana():
|
||||
response = urllib2.urlopen('http://www.iana.org/domains/reserved').read()
|
||||
assert 'Example domains' in response
|
||||
```
|
||||
|
||||
All of the parameters and configuration works the same for the decorator
|
||||
version.
|
||||
|
||||
## Configuration
|
||||
|
||||
If you don't like VCR's defaults, you can set options by instantiating a `VCR`
|
||||
class and setting the options on it.
|
||||
|
||||
```python
|
||||
|
||||
import vcr
|
||||
|
||||
my_vcr = vcr.VCR(
|
||||
serializer = 'json',
|
||||
cassette_library_dir = 'fixtures/cassettes',
|
||||
record_mode = 'once',
|
||||
match_on = ['uri', 'method'],
|
||||
)
|
||||
|
||||
with my_vcr.use_cassette('test.json'):
|
||||
# your http code here
|
||||
```
|
||||
|
||||
Otherwise, you can override options each time you use a cassette.
|
||||
|
||||
```python
|
||||
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.
|
||||
|
||||
## Request matching
|
||||
|
||||
Request matching is configurable and allows you to change which requests VCR
|
||||
considers identical. The default behavior is
|
||||
`['method', 'scheme', 'host', 'port', 'path', 'query']` 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)
|
||||
* uri (the full URI.)
|
||||
* host (the hostname of the server receiving the request)
|
||||
* port (the port of the server receiving the request)
|
||||
* path (the path of the request)
|
||||
* query (the query string of the request)
|
||||
* body (the entire request body)
|
||||
* headers (the headers of the request)
|
||||
|
||||
Backwards compatible matchers:
|
||||
* url (the `uri` alias)
|
||||
|
||||
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):
|
||||
|
||||
### 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 using to
|
||||
record your requests and responses. This will let you record your requests and
|
||||
responses and make assertions on them, to make sure that your code under test
|
||||
is generating the expected requests and responses. This feature is not present
|
||||
in Ruby's VCR, but I think it is a nice addition. Here's an example:
|
||||
|
||||
```python
|
||||
import vcr
|
||||
import urllib2
|
||||
|
||||
with vcr.use_cassette('fixtures/vcr_cassettes/synopsis.yaml') as cass:
|
||||
response = urllib2.urlopen('http://www.zombo.com/').read()
|
||||
# cass should have 1 request inside it
|
||||
assert len(cass) == 1
|
||||
# the request uri should have been http://www.zombo.com/
|
||||
assert cass.requests[0].uri == 'http://www.zombo.com/'
|
||||
```
|
||||
|
||||
The `Cassette` object exposes the following properties which I consider part of
|
||||
the API. The fields are as follows:
|
||||
|
||||
* `requests`: A list of vcr.Request objects corresponding to the http requests
|
||||
that were made during the recording of the cassette. The requests appear in the
|
||||
order that they were originally processed.
|
||||
* `responses`: A list of the responses made.
|
||||
* `play_count`: The number of times this cassette has played back a response.
|
||||
* `all_played`: A boolean indicating whether all the responses have been
|
||||
played back.
|
||||
* `responses_of(request)`: Access the responses that match a given request
|
||||
|
||||
The `Request` object has the following properties:
|
||||
|
||||
* `uri`: The full uri of the request. Example: "https://google.com/?q=vcrpy"
|
||||
* `scheme`: The scheme used to make the request (http or https)
|
||||
* `host`: The host of the request, for example "www.google.com"
|
||||
* `port`: The port the request was made on
|
||||
* `path`: The path of the request. For example "/" or "/home.html"
|
||||
* `query`: The parsed query string of the request. Sorted list of name, value pairs.
|
||||
* `method` : The method used to make the request, for example "GET" or "POST"
|
||||
* `body`: The body of the request, usually empty except for POST / PUT / etc
|
||||
|
||||
Backwards compatible properties:
|
||||
|
||||
* `url`: The `uri` alias
|
||||
* `protocol`: The `scheme` alias
|
||||
|
||||
|
||||
## Register your own serializer
|
||||
|
||||
Don't like JSON or YAML? That's OK, VCR.py can serialize to any format you
|
||||
would like. Create your own module or class instance with 2 methods:
|
||||
|
||||
* `def deserialize(cassette_string)`
|
||||
* `def serialize(cassette_dict)`
|
||||
|
||||
Finally, register your class with VCR to use your new serializer.
|
||||
|
||||
```python
|
||||
import vcr
|
||||
|
||||
class BogoSerializer(object):
|
||||
"""
|
||||
Must implement serialize() and deserialize() methods
|
||||
"""
|
||||
pass
|
||||
|
||||
my_vcr = vcr.VCR()
|
||||
my_vcr.register_serializer('bogo', BogoSerializer())
|
||||
|
||||
with my_vcr.use_cassette('test.bogo', serializer='bogo'):
|
||||
# your http here
|
||||
|
||||
# After you register, you can set the default serializer to your new serializer
|
||||
|
||||
my_vcr.serializer = 'bogo'
|
||||
|
||||
with my_vcr.use_cassette('test.bogo'):
|
||||
# your http here
|
||||
|
||||
```
|
||||
|
||||
## 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.
|
||||
|
||||
```python
|
||||
import vcr
|
||||
|
||||
def jurassic_matcher(r1, r2):
|
||||
return r1.uri == r2.uri 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
|
||||
|
||||
```
|
||||
|
||||
## Filter sensitive data from the request
|
||||
|
||||
If you are checking your cassettes into source control, and are using some form
|
||||
of authentication in your tests, you can filter out that information so it won't
|
||||
appear in your cassette files. There are a few ways to do this:
|
||||
|
||||
### Filter information from HTTP Headers
|
||||
Use the `filter_headers` configuration option with a list of headers to filter.
|
||||
|
||||
```python
|
||||
with my_vcr.use_cassette('test.yml', filter_headers=['authorization']):
|
||||
# sensitive HTTP request goes here
|
||||
```
|
||||
|
||||
### Filter information from HTTP querystring
|
||||
Use the `filter_query_parameters` configuration option with a list of query
|
||||
parameters to filter.
|
||||
|
||||
```python
|
||||
with my_vcr.use_cassette('test.yml', filter_query_parameters=['api_key']):
|
||||
requests.get('http://api.com/getdata?api_key=secretstring')
|
||||
```
|
||||
|
||||
### Filter information from HTTP post data
|
||||
Use the `filter_post_data_parameters` configuration option with a list of post data
|
||||
parameters to filter.
|
||||
|
||||
```python
|
||||
with my_vcr.use_cassette('test.yml', filter_post_data_parameters=['client_secret']):
|
||||
requests.post('http://api.com/postdata', data={'api_key': 'secretstring'})
|
||||
```
|
||||
|
||||
### Custom Request filtering
|
||||
|
||||
If none of these covers your request filtering needs, you can register a callback
|
||||
that will manipulate the HTTP request before adding it to the cassette. Use the
|
||||
`before_record` configuration option to so this. Here is an example that will
|
||||
never record requests to the /login endpoint.
|
||||
|
||||
```python
|
||||
def before_record_cb(request):
|
||||
if request.path != '/login':
|
||||
return request
|
||||
|
||||
my_vcr = vcr.VCR(
|
||||
before_record = before_record_cb,
|
||||
)
|
||||
with my_vcr.use_cassette('test.yml'):
|
||||
# your http code here
|
||||
```
|
||||
|
||||
You can also mutate the response using this callback. For example, you could
|
||||
remove all query parameters from any requests to the `'/login'` path.
|
||||
|
||||
```python
|
||||
def scrub_login_request(request):
|
||||
if request.path == '/login':
|
||||
request.uri, _ = urllib.splitquery(response.uri)
|
||||
return request
|
||||
|
||||
my_vcr = vcr.VCR(
|
||||
before_record=scrub_login_request,
|
||||
)
|
||||
with my_vcr.use_cassette('test.yml'):
|
||||
# your http code here
|
||||
```
|
||||
|
||||
### Custom Response Filtering
|
||||
|
||||
VCR.py also suports response filtering with the `before_record_response` keyword
|
||||
argument. It's usage is similar to that of `before_record`:
|
||||
|
||||
```python
|
||||
def scrub_string(string, replacement=''):
|
||||
def before_record_reponse(response):
|
||||
return response['body']['string'] = response['body']['string'].replace(string, replacement)
|
||||
return scrub_string
|
||||
|
||||
my_vcr = vcr.VCR(
|
||||
before_record=scrub_string(settings.USERNAME, 'username'),
|
||||
)
|
||||
with my_vcr.use_cassette('test.yml'):
|
||||
# your http code here
|
||||
```
|
||||
|
||||
## Ignore requests
|
||||
|
||||
If you would like to completely ignore certain requests, you can do it in a
|
||||
few ways:
|
||||
|
||||
* Set the `ignore_localhost` option equal to True. This will not record any
|
||||
requests sent to (or responses from) localhost, 127.0.0.1, or 0.0.0.0.
|
||||
* Set the `ignore_hosts` configuration option to a list of hosts to ignore
|
||||
* Add a `before_record` callback that returns None for requests you want to
|
||||
ignore
|
||||
|
||||
Requests that are ignored by VCR will not be saved in a cassette, nor played
|
||||
back from a cassette. VCR will completely ignore those requests as if it
|
||||
didn't notice them at all, and they will continue to hit the server as if VCR
|
||||
were not there.
|
||||
|
||||
## Custom Patches
|
||||
|
||||
If you use a custom `HTTPConnection` class, or otherwise make http
|
||||
requests in a way that requires additional patching, you can use the
|
||||
`custom_patches` keyword argument of the `VCR` and `Cassette` objects
|
||||
to patch those objects whenever a cassette's context is entered. To
|
||||
patch a custom version of `HTTPConnection` you can do something like
|
||||
this:
|
||||
|
||||
```
|
||||
import where_the_custom_https_connection_lives
|
||||
from vcr.stubs import VCRHTTPSConnection
|
||||
my_vcr = config.VCR(custom_patches=((where_the_custom_https_connection_lives, 'CustomHTTPSConnection', VCRHTTPSConnection),))
|
||||
|
||||
@my_vcr.use_cassette(...)
|
||||
```
|
||||
|
||||
## 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/)])
|
||||
|
||||
## Ruby VCR compatibility
|
||||
|
||||
VCR.py does not aim to match the format of the Ruby VCR YAML files. Cassettes
|
||||
generated by Ruby's VCR are not compatible with VCR.py.
|
||||
|
||||
## Running VCR's test suite
|
||||
|
||||
The tests are all run automatically on [Travis
|
||||
CI](https://travis-ci.org/kevin1024/vcrpy), but you can also run them yourself
|
||||
using [py.test](http://pytest.org/) and [Tox](http://tox.testrun.org/). Tox
|
||||
will automatically run them in all environments VCR.py supports. The test
|
||||
suite is pretty big and slow, but you can tell tox to only run specific tests
|
||||
like this:
|
||||
|
||||
`tox -e py27requests -- -v -k "'test_status_code or test_gzip'"`
|
||||
|
||||
This will run only tests that look like `test_status_code` or `test_gzip` in
|
||||
the test suite, and only in the python 2.7 environment that has `requests`
|
||||
installed.
|
||||
|
||||
Also, in order for the boto tests to run, you will need an AWS key. Refer to
|
||||
the [boto
|
||||
documentation](http://boto.readthedocs.org/en/latest/getting_started.html) for
|
||||
how to set this up. I have marked the boto tests as optional in Travis so you
|
||||
don't have to worry about them failing if you submit a pull request.
|
||||
|
||||
|
||||
## Logging
|
||||
VCR.py has a few log messages you can turn on to help you figure out if HTTP
|
||||
requests are hitting a real server or not. You can turn them on like this:
|
||||
|
||||
```python
|
||||
import vcr
|
||||
import requests
|
||||
import logging
|
||||
|
||||
logging.basicConfig() # you need to initialize logging, otherwise you will not see anything from vcrpy
|
||||
vcr_log = logging.getLogger("vcr")
|
||||
vcr_log.setLevel(logging.INFO)
|
||||
|
||||
with vcr.use_cassette('headers.yml'):
|
||||
requests.get('http://httpbin.org/headers')
|
||||
```
|
||||
|
||||
The first time you run this, you will see:
|
||||
|
||||
```
|
||||
INFO:vcr.stubs:<Request (GET) http://httpbin.org/headers> not in cassette, sending to real server
|
||||
```
|
||||
|
||||
The second time, you will see:
|
||||
|
||||
```
|
||||
INFO:vcr.stubs:Playing response for <Request (GET) http://httpbin.org/headers> from cassette
|
||||
```
|
||||
|
||||
If you set the loglevel to DEBUG, you will also get information about which
|
||||
matchers didn't match. This can help you with debugging custom matchers.
|
||||
|
||||
|
||||
## Upgrade
|
||||
|
||||
### New Cassette Format
|
||||
The cassette format has changed in _VCR.py 1.x_, the _VCR.py 0.x_ cassettes
|
||||
cannot be used with _VCR.py 1.x_. The easiest way to upgrade is to simply
|
||||
delete your cassettes and re-record all of them. VCR.py also provides a
|
||||
migration script that attempts to upgrade your 0.x cassettes to the new 1.x
|
||||
format. To use it, run the following command:
|
||||
|
||||
```
|
||||
python -m vcr.migration PATH
|
||||
```
|
||||
|
||||
The PATH can be either a path to the directory with cassettes or the path to a
|
||||
single cassette.
|
||||
|
||||
*Note*: Back up your cassettes files before migration.
|
||||
The migration *should* only modify cassettes using the old 0.x format.
|
||||
|
||||
## New serializer / deserializer API
|
||||
|
||||
If you made a custom serializer, you will need to update it to match the new
|
||||
API in version 1.0.x
|
||||
|
||||
* Serializers now take dicts and return strings.
|
||||
* Deserializers take strings and return dicts (instead of requests, responses
|
||||
pair)
|
||||
|
||||
|
||||
## Changelog
|
||||
* 1.4.0 Filter post data parameters (thanks @eadmundo), support for
|
||||
posting files through requests, inject_cassette kwarg to access
|
||||
cassette from `use_cassette` decorated function,
|
||||
`with_current_defaults` actually works (thanks @samstav).
|
||||
* 1.3.0 Fix/add support for urllib3 (thanks @aisch), fix default
|
||||
port for https (thanks @abhinav).
|
||||
* 1.2.0 Add custom_patches argument to VCR/Cassette objects to allow
|
||||
users to stub custom classes when cassettes become active.
|
||||
* 1.1.4 Add force reset around calls to actual connection from stubs, to ensure
|
||||
compatibility with the version of httplib/urlib2 in python 2.7.9.
|
||||
* 1.1.3 Fix python3 headers field (thanks @rtaboada), fix boto test (thanks
|
||||
@telaviv), fix new_episodes record mode (thanks @jashugan), fix Windows
|
||||
connectionpool stub bug (thanks @gazpachoking), add support for requests 2.5
|
||||
* 1.1.2 Add urllib==1.7.1 support. Make json serialize error handling correct
|
||||
Improve logging of match failures.
|
||||
* 1.1.1 Use function signature preserving `wrapt.decorator` to write the
|
||||
decorator version of use_cassette in order to ensure compatibility with
|
||||
py.test fixtures and python 2. Move all request filtering into the
|
||||
`before_record_callable`.
|
||||
* 1.1.0 Add `before_record_response`. Fix several bugs related to the context
|
||||
management of cassettes.
|
||||
* 1.0.3: Fix an issue with requests 2.4 and make sure case sensitivity is
|
||||
consistent across python versions
|
||||
* 1.0.2: Fix an issue with requests 2.3
|
||||
* 1.0.1: Fix a bug with the new ignore requests feature and the once
|
||||
record mode
|
||||
* 1.0.0: _BACKWARDS INCOMPATIBLE_: Please see the 'upgrade' section in the
|
||||
README. Take a look at the matcher section as well, you might want to
|
||||
update your `match_on` settings. Add support for filtering sensitive
|
||||
data from requests, matching query strings after the order changes and
|
||||
improving the built-in matchers, (thanks to @mshytikov), support for
|
||||
ignoring requests to certain hosts, bump supported Python3 version to
|
||||
3.4, fix some bugs with Boto support (thanks @marusich), fix error with
|
||||
URL field capitalization in README (thanks @simon-weber), added some log
|
||||
messages to help with debugging, added `all_played` property on cassette
|
||||
(thanks @mshytikov)
|
||||
* 0.7.0: VCR.py now supports Python 3! (thanks @asundg) Also I refactored
|
||||
the stub connections quite a bit to add support for the putrequest and
|
||||
putheader calls. This version also adds support for httplib2 (thanks
|
||||
@nilp0inter). I have added a couple tests for boto since it is an http
|
||||
client in its own right. Finally, this version includes a fix for a bug
|
||||
where requests wasn't being patched properly (thanks @msabramo).
|
||||
* 0.6.0: Store response headers as a list since a HTTP response can have the
|
||||
same header twice (happens with set-cookie sometimes). This has the added
|
||||
benefit of preserving the order of headers. Thanks @smallcode for the bug
|
||||
report leading to this change. I have made an effort to ensure backwards
|
||||
compatibility with the old cassettes' header storage mechanism, but if you
|
||||
want to upgrade to the new header storage, you should delete your
|
||||
cassettes and re-record them. Also this release adds better error messages
|
||||
(thanks @msabramo) and adds support for using VCR as a decorator (thanks
|
||||
@smallcode for the motivation)
|
||||
* 0.5.0: Change the `response_of` method to `responses_of` since cassettes
|
||||
can now contain more than one response for a request. Since this changes
|
||||
the API, I'm bumping the version. Also includes 2 bugfixes: a better error
|
||||
message when attempting to overwrite a cassette file, and a fix for a bug
|
||||
with requests sessions (thanks @msabramo)
|
||||
* 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 modes,
|
||||
and changed the default recording behavior to the "once" record 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 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.
|
||||
Thanks to @fatuhoku for some of the ideas and the motivation behind this
|
||||
release.
|
||||
* 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 haven't
|
||||
changed at all and supports JSON as well as YAML (thanks @sirpengi). Added
|
||||
amazing new skeumorphic logo, thanks @hairarrow.
|
||||
* 0.1.0: *backwards incompatible release - delete your old cassette files*:
|
||||
This release adds the ability to access the cassette to make assertions on
|
||||
it, as well as a major code refactor thanks to @dlecocq. It also fixes a
|
||||
couple longstanding bugs with redirects and HTTPS. [#3 and #4]
|
||||
* 0.0.4: If you have libyaml installed, vcrpy will use the c bindings
|
||||
instead. Speed up your tests! Thanks @dlecocq
|
||||
* 0.0.3: Add support for requests 1.2.3. Support for older versions of
|
||||
requests dropped (thanks @vitormazzi and @bryanhelmig)
|
||||
* 0.0.2: Add support for requests / urllib3
|
||||
* 0.0.1: Initial Release
|
||||
|
||||
# License
|
||||
|
||||
This library uses the MIT license. See [LICENSE.txt](LICENSE.txt) for more details
|
||||
782
README.rst
Normal file
782
README.rst
Normal file
@@ -0,0 +1,782 @@
|
||||
|Build Status| |Stories in Ready| |Gitter|
|
||||
|
||||
VCR.py
|
||||
======
|
||||
|
||||
.. image:: https://raw.github.com/kevin1024/vcrpy/master/vcr.png
|
||||
:alt: vcr.py
|
||||
|
||||
This is a Python version of `Ruby's VCR
|
||||
library <https://github.com/vcr/vcr>`__.
|
||||
|
||||
What it does
|
||||
------------
|
||||
|
||||
VCR.py simplifies and speeds up tests that make HTTP requests. The
|
||||
first time you run code that is inside a VCR.py context manager or
|
||||
decorated function, VCR.py records all HTTP interactions that take
|
||||
place through the libraries it supports and serializes and writes them
|
||||
to a flat file (in yaml format by default). This flat file is called a
|
||||
cassette. When the relevant peice of code is executed again, VCR.py
|
||||
will read the serialized requests and responses from the
|
||||
aforementioned cassette file, and intercept any HTTP requests that it
|
||||
recognizes from the original test run and return the responses that
|
||||
corresponded to those requests. This means that the requests will not
|
||||
actually result in HTTP traffic, which confers several benefits
|
||||
including:
|
||||
|
||||
- The ability to work offline
|
||||
- Completely deterministic tests
|
||||
- Increased test execution speed
|
||||
|
||||
If the server you are testing against ever changes its API, all you need
|
||||
to do is delete your existing cassette files, and run your tests again.
|
||||
VCR.py will detect the absence of a cassette file and once again record
|
||||
all HTTP interactions, which will update them to correspond to the new
|
||||
API.
|
||||
|
||||
Compatibility Notes
|
||||
-------------------
|
||||
|
||||
VCR.py supports Python 2.6 and 2.7, 3.3, 3.4, and
|
||||
`pypy <http://pypy.org>`__.
|
||||
|
||||
The following http libraries are supported:
|
||||
|
||||
- urllib2
|
||||
- urllib3
|
||||
- http.client (python3)
|
||||
- requests (both 1.x and 2.x versions)
|
||||
- httplib2
|
||||
- boto
|
||||
- Tornado's AsyncHTTPClient
|
||||
|
||||
Usage
|
||||
-----
|
||||
|
||||
.. code:: python
|
||||
|
||||
import vcr
|
||||
import urllib2
|
||||
|
||||
with vcr.use_cassette('fixtures/vcr_cassettes/synopsis.yaml'):
|
||||
response = urllib2.urlopen('http://www.iana.org/domains/reserved').read()
|
||||
assert 'Example domains' in response
|
||||
|
||||
Run this test once, and VCR.py will record the HTTP request to
|
||||
``fixtures/vcr_cassettes/synopsis.yml``. Run it again, and VCR.py will
|
||||
replay the response from iana.org when the http request is made. This
|
||||
test is now fast (no real HTTP requests are made anymore), deterministic
|
||||
(the test will continue to pass, even if you are offline, or iana.org
|
||||
goes down for maintenance) and accurate (the response will contain the
|
||||
same headers and body you get from a real request).
|
||||
|
||||
You can also use VCR.py as a decorator. The same request above would
|
||||
look like this:
|
||||
|
||||
.. code:: python
|
||||
|
||||
@vcr.use_cassette('fixtures/vcr_cassettes/synopsis.yaml')
|
||||
def test_iana():
|
||||
response = urllib2.urlopen('http://www.iana.org/domains/reserved').read()
|
||||
assert 'Example domains' in response
|
||||
|
||||
When using the decorator version of ``use_cassette``, it is possible to
|
||||
omit the path to the cassette file.
|
||||
|
||||
.. code:: python
|
||||
|
||||
@vcr.use_cassette()
|
||||
def test_iana():
|
||||
response = urllib2.urlopen('http://www.iana.org/domains/reserved').read()
|
||||
assert 'Example domains' in response
|
||||
|
||||
In this case, the cassette file will be given the same name as the test
|
||||
function, and it will be placed in the same directory as the file in
|
||||
which the test is defined. See the Automatic Test Naming section below
|
||||
for more details.
|
||||
|
||||
Configuration
|
||||
-------------
|
||||
|
||||
If you don't like VCR's defaults, you can set options by instantiating a
|
||||
``VCR`` class and setting the options on it.
|
||||
|
||||
.. code:: python
|
||||
|
||||
|
||||
import vcr
|
||||
|
||||
my_vcr = vcr.VCR(
|
||||
serializer='json',
|
||||
cassette_library_dir='fixtures/cassettes',
|
||||
record_mode='once',
|
||||
match_on=['uri', 'method'],
|
||||
)
|
||||
|
||||
with my_vcr.use_cassette('test.json'):
|
||||
# your http code here
|
||||
|
||||
Otherwise, you can override options each time you use a cassette.
|
||||
|
||||
.. code:: python
|
||||
|
||||
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.
|
||||
|
||||
Request matching
|
||||
----------------
|
||||
|
||||
Request matching is configurable and allows you to change which requests
|
||||
VCR considers identical. The default behavior is
|
||||
``['method', 'scheme', 'host', 'port', 'path', 'query']`` 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)
|
||||
- uri (the full URI.)
|
||||
- host (the hostname of the server receiving the request)
|
||||
- port (the port of the server receiving the request)
|
||||
- path (the path of the request)
|
||||
- query (the query string of the request)
|
||||
- raw\_body (the entire request body as is)
|
||||
- body (the entire request body unmarshalled by content-type
|
||||
i.e. xmlrpc, json, form-urlencoded, falling back on raw\_body)
|
||||
- headers (the headers of the request)
|
||||
|
||||
Backwards compatible matchers:
|
||||
- url (the ``uri`` alias)
|
||||
|
||||
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):
|
||||
|
||||
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
|
||||
using to record your requests and responses. This will let you record
|
||||
your requests and responses and make assertions on them, to make sure
|
||||
that your code under test is generating the expected requests and
|
||||
responses. This feature is not present in Ruby's VCR, but I think it is
|
||||
a nice addition. Here's an example:
|
||||
|
||||
.. code:: python
|
||||
|
||||
import vcr
|
||||
import urllib2
|
||||
|
||||
with vcr.use_cassette('fixtures/vcr_cassettes/synopsis.yaml') as cass:
|
||||
response = urllib2.urlopen('http://www.zombo.com/').read()
|
||||
# cass should have 1 request inside it
|
||||
assert len(cass) == 1
|
||||
# the request uri should have been http://www.zombo.com/
|
||||
assert cass.requests[0].uri == 'http://www.zombo.com/'
|
||||
|
||||
The ``Cassette`` object exposes the following properties which I
|
||||
consider part of the API. The fields are as follows:
|
||||
|
||||
- ``requests``: A list of vcr.Request objects corresponding to the http
|
||||
requests that were made during the recording of the cassette. The
|
||||
requests appear in the order that they were originally processed.
|
||||
- ``responses``: A list of the responses made.
|
||||
- ``play_count``: The number of times this cassette has played back a
|
||||
response.
|
||||
- ``all_played``: A boolean indicating whether all the responses have
|
||||
been played back.
|
||||
- ``responses_of(request)``: Access the responses that match a given
|
||||
request
|
||||
|
||||
The ``Request`` object has the following properties:
|
||||
|
||||
- ``uri``: The full uri of the request. Example:
|
||||
"https://google.com/?q=vcrpy"
|
||||
- ``scheme``: The scheme used to make the request (http or https)
|
||||
- ``host``: The host of the request, for example "www.google.com"
|
||||
- ``port``: The port the request was made on
|
||||
- ``path``: The path of the request. For example "/" or "/home.html"
|
||||
- ``query``: The parsed query string of the request. Sorted list of
|
||||
name, value pairs.
|
||||
- ``method`` : The method used to make the request, for example "GET"
|
||||
or "POST"
|
||||
- ``body``: The body of the request, usually empty except for POST /
|
||||
PUT / etc
|
||||
|
||||
Backwards compatible properties:
|
||||
|
||||
- ``url``: The ``uri`` alias
|
||||
- ``protocol``: The ``scheme`` alias
|
||||
|
||||
Register your own serializer
|
||||
----------------------------
|
||||
|
||||
Don't like JSON or YAML? That's OK, VCR.py can serialize to any format
|
||||
you would like. Create your own module or class instance with 2 methods:
|
||||
|
||||
- ``def deserialize(cassette_string)``
|
||||
- ``def serialize(cassette_dict)``
|
||||
|
||||
Finally, register your class with VCR to use your new serializer.
|
||||
|
||||
.. code:: python
|
||||
|
||||
import vcr
|
||||
|
||||
class BogoSerializer(object):
|
||||
"""
|
||||
Must implement serialize() and deserialize() methods
|
||||
"""
|
||||
pass
|
||||
|
||||
my_vcr = vcr.VCR()
|
||||
my_vcr.register_serializer('bogo', BogoSerializer())
|
||||
|
||||
with my_vcr.use_cassette('test.bogo', serializer='bogo'):
|
||||
# your http here
|
||||
|
||||
# After you register, you can set the default serializer to your new serializer
|
||||
|
||||
my_vcr.serializer = 'bogo'
|
||||
|
||||
with my_vcr.use_cassette('test.bogo'):
|
||||
# your http here
|
||||
|
||||
Register your own request matcher
|
||||
---------------------------------
|
||||
|
||||
Create your own method with the following signature
|
||||
|
||||
.. code:: 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.
|
||||
|
||||
.. code:: python
|
||||
|
||||
import vcr
|
||||
|
||||
def jurassic_matcher(r1, r2):
|
||||
return r1.uri == r2.uri 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
|
||||
|
||||
Filter sensitive data from the request
|
||||
--------------------------------------
|
||||
|
||||
If you are checking your cassettes into source control, and are using
|
||||
some form of authentication in your tests, you can filter out that
|
||||
information so it won't appear in your cassette files. There are a few
|
||||
ways to do this:
|
||||
|
||||
Filter information from HTTP Headers
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Use the ``filter_headers`` configuration option with a list of headers
|
||||
to filter.
|
||||
|
||||
.. code:: python
|
||||
|
||||
with my_vcr.use_cassette('test.yml', filter_headers=['authorization']):
|
||||
# sensitive HTTP request goes here
|
||||
|
||||
Filter information from HTTP querystring
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Use the ``filter_query_parameters`` configuration option with a list of
|
||||
query parameters to filter.
|
||||
|
||||
.. code:: python
|
||||
|
||||
with my_vcr.use_cassette('test.yml', filter_query_parameters=['api_key']):
|
||||
requests.get('http://api.com/getdata?api_key=secretstring')
|
||||
|
||||
Filter information from HTTP post data
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Use the ``filter_post_data_parameters`` configuration option with a list
|
||||
of post data parameters to filter.
|
||||
|
||||
.. code:: python
|
||||
|
||||
with my_vcr.use_cassette('test.yml', filter_post_data_parameters=['client_secret']):
|
||||
requests.post('http://api.com/postdata', data={'api_key': 'secretstring'})
|
||||
|
||||
Custom Request filtering
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
If none of these covers your request filtering needs, you can register a
|
||||
callback that will manipulate the HTTP request before adding it to the
|
||||
cassette. Use the ``before_record`` configuration option to so this.
|
||||
Here is an example that will never record requests to the /login
|
||||
endpoint.
|
||||
|
||||
.. code:: python
|
||||
|
||||
def before_record_cb(request):
|
||||
if request.path != '/login':
|
||||
return request
|
||||
|
||||
my_vcr = vcr.VCR(
|
||||
before_record = before_record_cb,
|
||||
)
|
||||
with my_vcr.use_cassette('test.yml'):
|
||||
# your http code here
|
||||
|
||||
You can also mutate the response using this callback. For example, you
|
||||
could remove all query parameters from any requests to the ``'/login'``
|
||||
path.
|
||||
|
||||
.. code:: python
|
||||
|
||||
def scrub_login_request(request):
|
||||
if request.path == '/login':
|
||||
request.uri, _ = urllib.splitquery(response.uri)
|
||||
return request
|
||||
|
||||
my_vcr = vcr.VCR(
|
||||
before_record=scrub_login_request,
|
||||
)
|
||||
with my_vcr.use_cassette('test.yml'):
|
||||
# your http code here
|
||||
|
||||
Custom Response Filtering
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
VCR.py also suports response filtering with the
|
||||
``before_record_response`` keyword argument. It's usage is similar to
|
||||
that of ``before_record``:
|
||||
|
||||
.. code:: python
|
||||
|
||||
def scrub_string(string, replacement=''):
|
||||
def before_record_response(response):
|
||||
response['body']['string'] = response['body']['string'].replace(string, replacement)
|
||||
return response
|
||||
return before_record_response
|
||||
|
||||
my_vcr = vcr.VCR(
|
||||
before_record_response=scrub_string(settings.USERNAME, 'username'),
|
||||
)
|
||||
with my_vcr.use_cassette('test.yml'):
|
||||
# your http code here
|
||||
|
||||
Ignore requests
|
||||
---------------
|
||||
|
||||
If you would like to completely ignore certain requests, you can do it
|
||||
in a few ways:
|
||||
|
||||
- Set the ``ignore_localhost`` option equal to True. This will not
|
||||
record any requests sent to (or responses from) localhost, 127.0.0.1,
|
||||
or 0.0.0.0.
|
||||
- Set the ``ignore_hosts`` configuration option to a list of hosts to
|
||||
ignore
|
||||
- Add a ``before_record`` callback that returns None for requests you
|
||||
want to ignore
|
||||
|
||||
Requests that are ignored by VCR will not be saved in a cassette, nor
|
||||
played back from a cassette. VCR will completely ignore those requests
|
||||
as if it didn't notice them at all, and they will continue to hit the
|
||||
server as if VCR were not there.
|
||||
|
||||
Custom Patches
|
||||
--------------
|
||||
|
||||
If you use a custom ``HTTPConnection`` class, or otherwise make http
|
||||
requests in a way that requires additional patching, you can use the
|
||||
``custom_patches`` keyword argument of the ``VCR`` and ``Cassette``
|
||||
objects to patch those objects whenever a cassette's context is entered.
|
||||
To patch a custom version of ``HTTPConnection`` you can do something
|
||||
like this:
|
||||
|
||||
::
|
||||
|
||||
import where_the_custom_https_connection_lives
|
||||
from vcr.stubs import VCRHTTPSConnection
|
||||
my_vcr = config.VCR(custom_patches=((where_the_custom_https_connection_lives, 'CustomHTTPSConnection', VCRHTTPSConnection),))
|
||||
|
||||
@my_vcr.use_cassette(...)
|
||||
|
||||
Automatic Cassette Naming
|
||||
-------------------------
|
||||
|
||||
VCR.py now allows the omission of the path argument to the use\_cassette
|
||||
function. Both of the following are now legal/should work
|
||||
|
||||
.. code:: python
|
||||
|
||||
@my_vcr.use_cassette
|
||||
def my_test_function():
|
||||
...
|
||||
|
||||
.. code:: python
|
||||
|
||||
@my_vcr.use_cassette()
|
||||
def my_test_function():
|
||||
...
|
||||
|
||||
In both cases, VCR.py will use a path that is generated from the
|
||||
provided test function's name. If no ``cassette_library_dir`` has been
|
||||
set, the cassette will be in a file with the name of the test function
|
||||
in directory of the file in which the test function is declared. If a
|
||||
``cassette_library_dir`` has been set, the cassette will appear in that
|
||||
directory in a file with the name of the decorated function.
|
||||
|
||||
It is possible to control the path produced by the automatic naming
|
||||
machinery by customizing the ``path_transformer`` and
|
||||
``func_path_generator`` vcr variables. To add an extension to all
|
||||
cassette names, use ``VCR.ensure_suffix`` as follows:
|
||||
|
||||
.. code:: python
|
||||
|
||||
my_vcr = VCR(path_transformer=VCR.ensure_suffix('.yaml'))
|
||||
|
||||
@my_vcr.use_cassette
|
||||
def my_test_function():
|
||||
|
||||
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/>`__\ ])
|
||||
|
||||
Ruby VCR compatibility
|
||||
----------------------
|
||||
|
||||
VCR.py does not aim to match the format of the Ruby VCR YAML files.
|
||||
Cassettes generated by Ruby's VCR are not compatible with VCR.py.
|
||||
|
||||
Running VCR's test suite
|
||||
------------------------
|
||||
|
||||
The tests are all run automatically on `Travis
|
||||
CI <https://travis-ci.org/kevin1024/vcrpy>`__, but you can also run them
|
||||
yourself using `py.test <http://pytest.org/>`__ and
|
||||
`Tox <http://tox.testrun.org/>`__. Tox will automatically run them in
|
||||
all environments VCR.py supports. The test suite is pretty big and slow,
|
||||
but you can tell tox to only run specific tests like this:
|
||||
|
||||
``tox -e py27requests -- -v -k "'test_status_code or test_gzip'"``
|
||||
|
||||
This will run only tests that look like ``test_status_code`` or
|
||||
``test_gzip`` in the test suite, and only in the python 2.7 environment
|
||||
that has ``requests`` installed.
|
||||
|
||||
Also, in order for the boto tests to run, you will need an AWS key.
|
||||
Refer to the `boto
|
||||
documentation <http://boto.readthedocs.org/en/latest/getting_started.html>`__
|
||||
for how to set this up. I have marked the boto tests as optional in
|
||||
Travis so you don't have to worry about them failing if you submit a
|
||||
pull request.
|
||||
|
||||
Logging
|
||||
-------
|
||||
|
||||
VCR.py has a few log messages you can turn on to help you figure out if
|
||||
HTTP requests are hitting a real server or not. You can turn them on
|
||||
like this:
|
||||
|
||||
.. code:: python
|
||||
|
||||
import vcr
|
||||
import requests
|
||||
import logging
|
||||
|
||||
logging.basicConfig() # you need to initialize logging, otherwise you will not see anything from vcrpy
|
||||
vcr_log = logging.getLogger("vcr")
|
||||
vcr_log.setLevel(logging.INFO)
|
||||
|
||||
with vcr.use_cassette('headers.yml'):
|
||||
requests.get('http://httpbin.org/headers')
|
||||
|
||||
The first time you run this, you will see:
|
||||
|
||||
::
|
||||
|
||||
INFO:vcr.stubs:<Request (GET) http://httpbin.org/headers> not in cassette, sending to real server
|
||||
|
||||
The second time, you will see:
|
||||
|
||||
::
|
||||
|
||||
INFO:vcr.stubs:Playing response for <Request (GET) http://httpbin.org/headers> from cassette
|
||||
|
||||
If you set the loglevel to DEBUG, you will also get information about
|
||||
which matchers didn't match. This can help you with debugging custom
|
||||
matchers.
|
||||
|
||||
Speed
|
||||
-----
|
||||
VCR.py runs about 10x faster when pyyaml can use the libyaml extensions. However, just installing ``libyaml`` (Mac) or ``libyaml-dev`` (Linux) is not enough, as pyyaml needs to be rebuild with the proper flag. Note that this flag is cached by pip, so clear the cache first.
|
||||
|
||||
Are you using libyaml already? This should work:
|
||||
|
||||
.. code:: sh
|
||||
|
||||
python -c 'from yaml import CLoader'
|
||||
|
||||
If not:
|
||||
|
||||
.. code:: sh
|
||||
|
||||
pip uninstall pyyaml
|
||||
pip --no-cache-dir install pyyaml
|
||||
|
||||
|
||||
Upgrade
|
||||
-------
|
||||
|
||||
New Cassette Format
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
The cassette format has changed in *VCR.py 1.x*, the *VCR.py 0.x*
|
||||
cassettes cannot be used with *VCR.py 1.x*. The easiest way to upgrade
|
||||
is to simply delete your cassettes and re-record all of them. VCR.py
|
||||
also provides a migration script that attempts to upgrade your 0.x
|
||||
cassettes to the new 1.x format. To use it, run the following command:
|
||||
|
||||
::
|
||||
|
||||
python -m vcr.migration PATH
|
||||
|
||||
The PATH can be either a path to the directory with cassettes or the
|
||||
path to a single cassette.
|
||||
|
||||
*Note*: Back up your cassettes files before migration. The migration
|
||||
*should* only modify cassettes using the old 0.x format.
|
||||
|
||||
New serializer / deserializer API
|
||||
---------------------------------
|
||||
|
||||
If you made a custom serializer, you will need to update it to match the
|
||||
new API in version 1.0.x
|
||||
|
||||
- Serializers now take dicts and return strings.
|
||||
- Deserializers take strings and return dicts (instead of requests,
|
||||
responses pair)
|
||||
|
||||
Changelog
|
||||
---------
|
||||
- 1.7.4 [#217] Make use_cassette decorated functions actually return a
|
||||
value (thanks @bcen). [#199] Fix path transfromation defaults.
|
||||
Better headers dictionary management.
|
||||
- 1.7.3 [#188] ``additional_matchers`` kwarg on ``use_cassette``.
|
||||
[#191] Actually support passing multiple before_record_request
|
||||
functions (thanks @agriffis).
|
||||
- 1.7.2 [#186] Get effective_url in tornado (thanks @mvschaik), [#187]
|
||||
Set request_time on Response object in tornado (thanks @abhinav).
|
||||
- 1.7.1 [#183] Patch ``fetch_impl`` instead of the entire HTTPClient
|
||||
class for Tornado (thanks @abhinav).
|
||||
- 1.7.0 [#177] Properly support coroutine/generator decoration. [#178]
|
||||
Support distribute (thanks @graingert). [#163] Make compatibility
|
||||
between python2 and python3 recorded cassettes more robust (thanks
|
||||
@gward).
|
||||
- 1.6.1 [#169] Support conditional requirements in old versions of
|
||||
pip, Fix RST parse errors generated by pandoc, [Tornado] Fix
|
||||
unsupported features exception not being raised, [#166]
|
||||
content-aware body matcher.
|
||||
- 1.6.0 [#120] Tornado support (thanks @abhinav), [#147] packaging fixes
|
||||
(thanks @graingert), [#158] allow filtering post params in requests
|
||||
(thanks @MrJohz), [#140] add xmlrpclib support (thanks @Diaoul).
|
||||
- 1.5.2 Fix crash when cassette path contains cassette library
|
||||
directory (thanks @gazpachoking).
|
||||
- 1.5.0 Automatic cassette naming and 'application/json' post data
|
||||
filtering (thanks @marco-santamaria).
|
||||
- 1.4.2 Fix a bug caused by requests 2.7 and chunked transfer encoding
|
||||
- 1.4.1 Include README, tests, LICENSE in package. Thanks @ralphbean.
|
||||
- 1.4.0 Filter post data parameters (thanks @eadmundo), support for
|
||||
posting files through requests, inject\_cassette kwarg to access
|
||||
cassette from ``use_cassette`` decorated function,
|
||||
``with_current_defaults`` actually works (thanks @samstav).
|
||||
- 1.3.0 Fix/add support for urllib3 (thanks @aisch), fix default port
|
||||
for https (thanks @abhinav).
|
||||
- 1.2.0 Add custom\_patches argument to VCR/Cassette objects to allow
|
||||
users to stub custom classes when cassettes become active.
|
||||
- 1.1.4 Add force reset around calls to actual connection from stubs,
|
||||
to ensure compatibility with the version of httplib/urlib2 in python
|
||||
2.7.9.
|
||||
- 1.1.3 Fix python3 headers field (thanks @rtaboada), fix boto test
|
||||
(thanks @telaviv), fix new\_episodes record mode (thanks @jashugan),
|
||||
fix Windows connectionpool stub bug (thanks @gazpachoking), add
|
||||
support for requests 2.5
|
||||
- 1.1.2 Add urllib==1.7.1 support. Make json serialize error handling
|
||||
correct Improve logging of match failures.
|
||||
- 1.1.1 Use function signature preserving ``wrapt.decorator`` to write
|
||||
the decorator version of use\_cassette in order to ensure
|
||||
compatibility with py.test fixtures and python 2. Move all request
|
||||
filtering into the ``before_record_callable``.
|
||||
- 1.1.0 Add ``before_record_response``. Fix several bugs related to the
|
||||
context management of cassettes.
|
||||
- 1.0.3: Fix an issue with requests 2.4 and make sure case sensitivity
|
||||
is consistent across python versions
|
||||
- 1.0.2: Fix an issue with requests 2.3
|
||||
- 1.0.1: Fix a bug with the new ignore requests feature and the once
|
||||
record mode
|
||||
- 1.0.0: *BACKWARDS INCOMPATIBLE*: Please see the 'upgrade' section in
|
||||
the README. Take a look at the matcher section as well, you might
|
||||
want to update your ``match_on`` settings. Add support for filtering
|
||||
sensitive data from requests, matching query strings after the order
|
||||
changes and improving the built-in matchers, (thanks to @mshytikov),
|
||||
support for ignoring requests to certain hosts, bump supported
|
||||
Python3 version to 3.4, fix some bugs with Boto support (thanks
|
||||
@marusich), fix error with URL field capitalization in README (thanks
|
||||
@simon-weber), added some log messages to help with debugging, added
|
||||
``all_played`` property on cassette (thanks @mshytikov)
|
||||
- 0.7.0: VCR.py now supports Python 3! (thanks @asundg) Also I
|
||||
refactored the stub connections quite a bit to add support for the
|
||||
putrequest and putheader calls. This version also adds support for
|
||||
httplib2 (thanks @nilp0inter). I have added a couple tests for boto
|
||||
since it is an http client in its own right. Finally, this version
|
||||
includes a fix for a bug where requests wasn't being patched properly
|
||||
(thanks @msabramo).
|
||||
- 0.6.0: Store response headers as a list since a HTTP response can
|
||||
have the same header twice (happens with set-cookie sometimes). This
|
||||
has the added benefit of preserving the order of headers. Thanks
|
||||
@smallcode for the bug report leading to this change. I have made an
|
||||
effort to ensure backwards compatibility with the old cassettes'
|
||||
header storage mechanism, but if you want to upgrade to the new
|
||||
header storage, you should delete your cassettes and re-record them.
|
||||
Also this release adds better error messages (thanks @msabramo) and
|
||||
adds support for using VCR as a decorator (thanks @smallcode for the
|
||||
motivation)
|
||||
- 0.5.0: Change the ``response_of`` method to ``responses_of`` since
|
||||
cassettes can now contain more than one response for a request. Since
|
||||
this changes the API, I'm bumping the version. Also includes 2
|
||||
bugfixes: a better error message when attempting to overwrite a
|
||||
cassette file, and a fix for a bug with requests sessions (thanks
|
||||
@msabramo)
|
||||
- 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
|
||||
modes, and changed the default recording behavior to the "once"
|
||||
record 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 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. Thanks to @fatuhoku
|
||||
for some of the ideas and the motivation behind this release.
|
||||
- 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 haven't changed at all and supports JSON as well as YAML
|
||||
(thanks @sirpengi). Added amazing new skeumorphic logo, thanks
|
||||
@hairarrow.
|
||||
- 0.1.0: *backwards incompatible release - delete your old cassette
|
||||
files*: This release adds the ability to access the cassette to make
|
||||
assertions on it, as well as a major code refactor thanks to
|
||||
@dlecocq. It also fixes a couple longstanding bugs with redirects and
|
||||
HTTPS. [#3 and #4]
|
||||
- 0.0.4: If you have libyaml installed, vcrpy will use the c bindings
|
||||
instead. Speed up your tests! Thanks @dlecocq
|
||||
- 0.0.3: Add support for requests 1.2.3. Support for older versions of
|
||||
requests dropped (thanks @vitormazzi and @bryanhelmig)
|
||||
- 0.0.2: Add support for requests / urllib3
|
||||
- 0.0.1: Initial Release
|
||||
|
||||
License
|
||||
=======
|
||||
|
||||
This library uses the MIT license. See `LICENSE.txt <LICENSE.txt>`__ for
|
||||
more details
|
||||
|
||||
.. |Build Status| image:: https://secure.travis-ci.org/kevin1024/vcrpy.png?branch=master
|
||||
:target: http://travis-ci.org/kevin1024/vcrpy
|
||||
.. |Stories in Ready| image:: https://badge.waffle.io/kevin1024/vcrpy.png?label=ready&title=Ready
|
||||
:target: https://waffle.io/kevin1024/vcrpy
|
||||
.. |Gitter| image:: https://badges.gitter.im/Join%20Chat.svg
|
||||
:alt: Join the chat at https://gitter.im/kevin1024/vcrpy
|
||||
:target: https://gitter.im/kevin1024/vcrpy?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge
|
||||
192
docs/Makefile
Normal file
192
docs/Makefile
Normal file
@@ -0,0 +1,192 @@
|
||||
# Makefile for Sphinx documentation
|
||||
#
|
||||
|
||||
# You can set these variables from the command line.
|
||||
SPHINXOPTS =
|
||||
SPHINXBUILD = sphinx-build
|
||||
PAPER =
|
||||
BUILDDIR = _build
|
||||
|
||||
# User-friendly check for sphinx-build
|
||||
ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1)
|
||||
$(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/)
|
||||
endif
|
||||
|
||||
# Internal variables.
|
||||
PAPEROPT_a4 = -D latex_paper_size=a4
|
||||
PAPEROPT_letter = -D latex_paper_size=letter
|
||||
ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
|
||||
# the i18n builder cannot share the environment and doctrees with the others
|
||||
I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
|
||||
|
||||
.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest coverage gettext
|
||||
|
||||
help:
|
||||
@echo "Please use \`make <target>' where <target> is one of"
|
||||
@echo " html to make standalone HTML files"
|
||||
@echo " dirhtml to make HTML files named index.html in directories"
|
||||
@echo " singlehtml to make a single large HTML file"
|
||||
@echo " pickle to make pickle files"
|
||||
@echo " json to make JSON files"
|
||||
@echo " htmlhelp to make HTML files and a HTML help project"
|
||||
@echo " qthelp to make HTML files and a qthelp project"
|
||||
@echo " applehelp to make an Apple Help Book"
|
||||
@echo " devhelp to make HTML files and a Devhelp project"
|
||||
@echo " epub to make an epub"
|
||||
@echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
|
||||
@echo " latexpdf to make LaTeX files and run them through pdflatex"
|
||||
@echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx"
|
||||
@echo " text to make text files"
|
||||
@echo " man to make manual pages"
|
||||
@echo " texinfo to make Texinfo files"
|
||||
@echo " info to make Texinfo files and run them through makeinfo"
|
||||
@echo " gettext to make PO message catalogs"
|
||||
@echo " changes to make an overview of all changed/added/deprecated items"
|
||||
@echo " xml to make Docutils-native XML files"
|
||||
@echo " pseudoxml to make pseudoxml-XML files for display purposes"
|
||||
@echo " linkcheck to check all external links for integrity"
|
||||
@echo " doctest to run all doctests embedded in the documentation (if enabled)"
|
||||
@echo " coverage to run coverage check of the documentation (if enabled)"
|
||||
|
||||
clean:
|
||||
rm -rf $(BUILDDIR)/*
|
||||
|
||||
html:
|
||||
$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
|
||||
@echo
|
||||
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
|
||||
|
||||
dirhtml:
|
||||
$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
|
||||
@echo
|
||||
@echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
|
||||
|
||||
singlehtml:
|
||||
$(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
|
||||
@echo
|
||||
@echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
|
||||
|
||||
pickle:
|
||||
$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
|
||||
@echo
|
||||
@echo "Build finished; now you can process the pickle files."
|
||||
|
||||
json:
|
||||
$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
|
||||
@echo
|
||||
@echo "Build finished; now you can process the JSON files."
|
||||
|
||||
htmlhelp:
|
||||
$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
|
||||
@echo
|
||||
@echo "Build finished; now you can run HTML Help Workshop with the" \
|
||||
".hhp project file in $(BUILDDIR)/htmlhelp."
|
||||
|
||||
qthelp:
|
||||
$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
|
||||
@echo
|
||||
@echo "Build finished; now you can run "qcollectiongenerator" with the" \
|
||||
".qhcp project file in $(BUILDDIR)/qthelp, like this:"
|
||||
@echo "# qcollectiongenerator $(BUILDDIR)/qthelp/vcrpy.qhcp"
|
||||
@echo "To view the help file:"
|
||||
@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/vcrpy.qhc"
|
||||
|
||||
applehelp:
|
||||
$(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp
|
||||
@echo
|
||||
@echo "Build finished. The help book is in $(BUILDDIR)/applehelp."
|
||||
@echo "N.B. You won't be able to view it unless you put it in" \
|
||||
"~/Library/Documentation/Help or install it in your application" \
|
||||
"bundle."
|
||||
|
||||
devhelp:
|
||||
$(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
|
||||
@echo
|
||||
@echo "Build finished."
|
||||
@echo "To view the help file:"
|
||||
@echo "# mkdir -p $$HOME/.local/share/devhelp/vcrpy"
|
||||
@echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/vcrpy"
|
||||
@echo "# devhelp"
|
||||
|
||||
epub:
|
||||
$(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
|
||||
@echo
|
||||
@echo "Build finished. The epub file is in $(BUILDDIR)/epub."
|
||||
|
||||
latex:
|
||||
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
|
||||
@echo
|
||||
@echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
|
||||
@echo "Run \`make' in that directory to run these through (pdf)latex" \
|
||||
"(use \`make latexpdf' here to do that automatically)."
|
||||
|
||||
latexpdf:
|
||||
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
|
||||
@echo "Running LaTeX files through pdflatex..."
|
||||
$(MAKE) -C $(BUILDDIR)/latex all-pdf
|
||||
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
|
||||
|
||||
latexpdfja:
|
||||
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
|
||||
@echo "Running LaTeX files through platex and dvipdfmx..."
|
||||
$(MAKE) -C $(BUILDDIR)/latex all-pdf-ja
|
||||
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
|
||||
|
||||
text:
|
||||
$(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
|
||||
@echo
|
||||
@echo "Build finished. The text files are in $(BUILDDIR)/text."
|
||||
|
||||
man:
|
||||
$(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
|
||||
@echo
|
||||
@echo "Build finished. The manual pages are in $(BUILDDIR)/man."
|
||||
|
||||
texinfo:
|
||||
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
|
||||
@echo
|
||||
@echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
|
||||
@echo "Run \`make' in that directory to run these through makeinfo" \
|
||||
"(use \`make info' here to do that automatically)."
|
||||
|
||||
info:
|
||||
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
|
||||
@echo "Running Texinfo files through makeinfo..."
|
||||
make -C $(BUILDDIR)/texinfo info
|
||||
@echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
|
||||
|
||||
gettext:
|
||||
$(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
|
||||
@echo
|
||||
@echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
|
||||
|
||||
changes:
|
||||
$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
|
||||
@echo
|
||||
@echo "The overview file is in $(BUILDDIR)/changes."
|
||||
|
||||
linkcheck:
|
||||
$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
|
||||
@echo
|
||||
@echo "Link check complete; look for any errors in the above output " \
|
||||
"or in $(BUILDDIR)/linkcheck/output.txt."
|
||||
|
||||
doctest:
|
||||
$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
|
||||
@echo "Testing of doctests in the sources finished, look at the " \
|
||||
"results in $(BUILDDIR)/doctest/output.txt."
|
||||
|
||||
coverage:
|
||||
$(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage
|
||||
@echo "Testing of coverage in the sources finished, look at the " \
|
||||
"results in $(BUILDDIR)/coverage/python.txt."
|
||||
|
||||
xml:
|
||||
$(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml
|
||||
@echo
|
||||
@echo "Build finished. The XML files are in $(BUILDDIR)/xml."
|
||||
|
||||
pseudoxml:
|
||||
$(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml
|
||||
@echo
|
||||
@echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml."
|
||||
293
docs/conf.py
Normal file
293
docs/conf.py
Normal file
@@ -0,0 +1,293 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# vcrpy documentation build configuration file, created by
|
||||
# sphinx-quickstart on Sun Sep 13 11:18:00 2015.
|
||||
#
|
||||
# This file is execfile()d with the current directory set to its
|
||||
# containing dir.
|
||||
#
|
||||
# Note that not all possible configuration values are present in this
|
||||
# autogenerated file.
|
||||
#
|
||||
# All configuration values have a default; values that are commented out
|
||||
# serve to show the default.
|
||||
|
||||
import sys
|
||||
import os
|
||||
import shlex
|
||||
|
||||
# If extensions (or modules to document with autodoc) are in another directory,
|
||||
# add these directories to sys.path here. If the directory is relative to the
|
||||
# documentation root, use os.path.abspath to make it absolute, like shown here.
|
||||
#sys.path.insert(0, os.path.abspath('.'))
|
||||
|
||||
# -- General configuration ------------------------------------------------
|
||||
|
||||
# If your documentation needs a minimal Sphinx version, state it here.
|
||||
#needs_sphinx = '1.0'
|
||||
|
||||
# Add any Sphinx extension module names here, as strings. They can be
|
||||
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
|
||||
# ones.
|
||||
extensions = [
|
||||
'sphinx.ext.autodoc',
|
||||
'sphinx.ext.intersphinx',
|
||||
'sphinx.ext.coverage',
|
||||
'sphinx.ext.viewcode',
|
||||
]
|
||||
|
||||
# Add any paths that contain templates here, relative to this directory.
|
||||
templates_path = ['_templates']
|
||||
|
||||
# The suffix(es) of source filenames.
|
||||
# You can specify multiple suffix as a list of string:
|
||||
# source_suffix = ['.rst', '.md']
|
||||
source_suffix = '.rst'
|
||||
|
||||
# The encoding of source files.
|
||||
#source_encoding = 'utf-8-sig'
|
||||
|
||||
# The master toctree document.
|
||||
master_doc = 'index'
|
||||
|
||||
# General information about the project.
|
||||
project = u'vcrpy'
|
||||
copyright = u'2015, Kevin McCarthy'
|
||||
author = u'Kevin McCarthy'
|
||||
|
||||
# The version info for the project you're documenting, acts as replacement for
|
||||
# |version| and |release|, also used in various other places throughout the
|
||||
# built documents.
|
||||
#
|
||||
# The short X.Y version.
|
||||
version = '1.7.3'
|
||||
# The full version, including alpha/beta/rc tags.
|
||||
release = '1.7.3'
|
||||
|
||||
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||
# for a list of supported languages.
|
||||
#
|
||||
# This is also used if you do content translation via gettext catalogs.
|
||||
# Usually you set "language" from the command line for these cases.
|
||||
language = None
|
||||
|
||||
# There are two options for replacing |today|: either, you set today to some
|
||||
# non-false value, then it is used:
|
||||
#today = ''
|
||||
# Else, today_fmt is used as the format for a strftime call.
|
||||
#today_fmt = '%B %d, %Y'
|
||||
|
||||
# List of patterns, relative to source directory, that match files and
|
||||
# directories to ignore when looking for source files.
|
||||
exclude_patterns = ['_build']
|
||||
|
||||
# The reST default role (used for this markup: `text`) to use for all
|
||||
# documents.
|
||||
#default_role = None
|
||||
|
||||
# If true, '()' will be appended to :func: etc. cross-reference text.
|
||||
#add_function_parentheses = True
|
||||
|
||||
# If true, the current module name will be prepended to all description
|
||||
# unit titles (such as .. function::).
|
||||
#add_module_names = True
|
||||
|
||||
# If true, sectionauthor and moduleauthor directives will be shown in the
|
||||
# output. They are ignored by default.
|
||||
#show_authors = False
|
||||
|
||||
# The name of the Pygments (syntax highlighting) style to use.
|
||||
pygments_style = 'sphinx'
|
||||
|
||||
# A list of ignored prefixes for module index sorting.
|
||||
#modindex_common_prefix = []
|
||||
|
||||
# If true, keep warnings as "system message" paragraphs in the built documents.
|
||||
#keep_warnings = False
|
||||
|
||||
# If true, `todo` and `todoList` produce output, else they produce nothing.
|
||||
todo_include_todos = False
|
||||
|
||||
|
||||
# -- Options for HTML output ----------------------------------------------
|
||||
|
||||
# The theme to use for HTML and HTML Help pages. See the documentation for
|
||||
# a list of builtin themes.
|
||||
html_theme = 'alabaster'
|
||||
|
||||
# Theme options are theme-specific and customize the look and feel of a theme
|
||||
# further. For a list of options available for each theme, see the
|
||||
# documentation.
|
||||
#html_theme_options = {}
|
||||
|
||||
# Add any paths that contain custom themes here, relative to this directory.
|
||||
#html_theme_path = []
|
||||
|
||||
# The name for this set of Sphinx documents. If None, it defaults to
|
||||
# "<project> v<release> documentation".
|
||||
#html_title = None
|
||||
|
||||
# A shorter title for the navigation bar. Default is the same as html_title.
|
||||
#html_short_title = None
|
||||
|
||||
# The name of an image file (relative to this directory) to place at the top
|
||||
# of the sidebar.
|
||||
#html_logo = None
|
||||
|
||||
# The name of an image file (within the static path) to use as favicon of the
|
||||
# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
|
||||
# pixels large.
|
||||
#html_favicon = None
|
||||
|
||||
# Add any paths that contain custom static files (such as style sheets) here,
|
||||
# relative to this directory. They are copied after the builtin static files,
|
||||
# so a file named "default.css" will overwrite the builtin "default.css".
|
||||
html_static_path = ['_static']
|
||||
|
||||
# Add any extra paths that contain custom files (such as robots.txt or
|
||||
# .htaccess) here, relative to this directory. These files are copied
|
||||
# directly to the root of the documentation.
|
||||
#html_extra_path = []
|
||||
|
||||
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
|
||||
# using the given strftime format.
|
||||
#html_last_updated_fmt = '%b %d, %Y'
|
||||
|
||||
# If true, SmartyPants will be used to convert quotes and dashes to
|
||||
# typographically correct entities.
|
||||
#html_use_smartypants = True
|
||||
|
||||
# Custom sidebar templates, maps document names to template names.
|
||||
#html_sidebars = {}
|
||||
|
||||
# Additional templates that should be rendered to pages, maps page names to
|
||||
# template names.
|
||||
#html_additional_pages = {}
|
||||
|
||||
# If false, no module index is generated.
|
||||
#html_domain_indices = True
|
||||
|
||||
# If false, no index is generated.
|
||||
#html_use_index = True
|
||||
|
||||
# If true, the index is split into individual pages for each letter.
|
||||
#html_split_index = False
|
||||
|
||||
# If true, links to the reST sources are added to the pages.
|
||||
#html_show_sourcelink = True
|
||||
|
||||
# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
|
||||
#html_show_sphinx = True
|
||||
|
||||
# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
|
||||
#html_show_copyright = True
|
||||
|
||||
# If true, an OpenSearch description file will be output, and all pages will
|
||||
# contain a <link> tag referring to it. The value of this option must be the
|
||||
# base URL from which the finished HTML is served.
|
||||
#html_use_opensearch = ''
|
||||
|
||||
# This is the file name suffix for HTML files (e.g. ".xhtml").
|
||||
#html_file_suffix = None
|
||||
|
||||
# Language to be used for generating the HTML full-text search index.
|
||||
# Sphinx supports the following languages:
|
||||
# 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja'
|
||||
# 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr'
|
||||
#html_search_language = 'en'
|
||||
|
||||
# A dictionary with options for the search language support, empty by default.
|
||||
# Now only 'ja' uses this config value
|
||||
#html_search_options = {'type': 'default'}
|
||||
|
||||
# The name of a javascript file (relative to the configuration directory) that
|
||||
# implements a search results scorer. If empty, the default will be used.
|
||||
#html_search_scorer = 'scorer.js'
|
||||
|
||||
# Output file base name for HTML help builder.
|
||||
htmlhelp_basename = 'vcrpydoc'
|
||||
|
||||
# -- Options for LaTeX output ---------------------------------------------
|
||||
|
||||
latex_elements = {
|
||||
# The paper size ('letterpaper' or 'a4paper').
|
||||
#'papersize': 'letterpaper',
|
||||
|
||||
# The font size ('10pt', '11pt' or '12pt').
|
||||
#'pointsize': '10pt',
|
||||
|
||||
# Additional stuff for the LaTeX preamble.
|
||||
#'preamble': '',
|
||||
|
||||
# Latex figure (float) alignment
|
||||
#'figure_align': 'htbp',
|
||||
}
|
||||
|
||||
# Grouping the document tree into LaTeX files. List of tuples
|
||||
# (source start file, target name, title,
|
||||
# author, documentclass [howto, manual, or own class]).
|
||||
latex_documents = [
|
||||
(master_doc, 'vcrpy.tex', u'vcrpy Documentation',
|
||||
u'Kevin McCarthy', 'manual'),
|
||||
]
|
||||
|
||||
# The name of an image file (relative to this directory) to place at the top of
|
||||
# the title page.
|
||||
#latex_logo = None
|
||||
|
||||
# For "manual" documents, if this is true, then toplevel headings are parts,
|
||||
# not chapters.
|
||||
#latex_use_parts = False
|
||||
|
||||
# If true, show page references after internal links.
|
||||
#latex_show_pagerefs = False
|
||||
|
||||
# If true, show URL addresses after external links.
|
||||
#latex_show_urls = False
|
||||
|
||||
# Documents to append as an appendix to all manuals.
|
||||
#latex_appendices = []
|
||||
|
||||
# If false, no module index is generated.
|
||||
#latex_domain_indices = True
|
||||
|
||||
|
||||
# -- Options for manual page output ---------------------------------------
|
||||
|
||||
# One entry per manual page. List of tuples
|
||||
# (source start file, name, description, authors, manual section).
|
||||
man_pages = [
|
||||
(master_doc, 'vcrpy', u'vcrpy Documentation',
|
||||
[author], 1)
|
||||
]
|
||||
|
||||
# If true, show URL addresses after external links.
|
||||
#man_show_urls = False
|
||||
|
||||
|
||||
# -- Options for Texinfo output -------------------------------------------
|
||||
|
||||
# Grouping the document tree into Texinfo files. List of tuples
|
||||
# (source start file, target name, title, author,
|
||||
# dir menu entry, description, category)
|
||||
texinfo_documents = [
|
||||
(master_doc, 'vcrpy', u'vcrpy Documentation',
|
||||
author, 'vcrpy', 'One line description of project.',
|
||||
'Miscellaneous'),
|
||||
]
|
||||
|
||||
# Documents to append as an appendix to all manuals.
|
||||
#texinfo_appendices = []
|
||||
|
||||
# If false, no module index is generated.
|
||||
#texinfo_domain_indices = True
|
||||
|
||||
# How to display URL addresses: 'footnote', 'no', or 'inline'.
|
||||
#texinfo_show_urls = 'footnote'
|
||||
|
||||
# If true, do not generate a @detailmenu in the "Top" node's menu.
|
||||
#texinfo_no_detailmenu = False
|
||||
|
||||
|
||||
# Example configuration for intersphinx: refer to the Python standard library.
|
||||
intersphinx_mapping = {'https://docs.python.org/': None}
|
||||
19
docs/index.rst
Normal file
19
docs/index.rst
Normal file
@@ -0,0 +1,19 @@
|
||||
vcrpy
|
||||
=====
|
||||
|
||||
Contents:
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
vcr
|
||||
|
||||
|
||||
|
||||
Indices and tables
|
||||
==================
|
||||
|
||||
* :ref:`genindex`
|
||||
* :ref:`modindex`
|
||||
* :ref:`search`
|
||||
|
||||
6
docs/vcr.rst
Normal file
6
docs/vcr.rst
Normal file
@@ -0,0 +1,6 @@
|
||||
:mod:`~vcr.config`
|
||||
=================
|
||||
|
||||
.. automodule:: vcr.config
|
||||
:members:
|
||||
:special-members: __init__
|
||||
38
setup.py
38
setup.py
@@ -1,8 +1,13 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
import sys
|
||||
import logging
|
||||
|
||||
from setuptools import setup, find_packages
|
||||
from setuptools.command.test import test as TestCommand
|
||||
import pkg_resources
|
||||
|
||||
long_description = open('README.rst', 'r').read()
|
||||
|
||||
|
||||
class PyTest(TestCommand):
|
||||
@@ -18,19 +23,46 @@ class PyTest(TestCommand):
|
||||
errno = pytest.main(self.test_args)
|
||||
sys.exit(errno)
|
||||
|
||||
|
||||
install_requires = ['PyYAML', 'wrapt', 'six>=1.5']
|
||||
|
||||
|
||||
extras_require = {
|
||||
':python_version in "2.4, 2.5, 2.6"':
|
||||
['contextlib2', 'backport_collections', 'mock'],
|
||||
':python_version in "2.7, 3.1, 3.2"': ['contextlib2', 'mock'],
|
||||
}
|
||||
|
||||
|
||||
try:
|
||||
if 'bdist_wheel' not in sys.argv:
|
||||
for key, value in extras_require.items():
|
||||
if key.startswith(':') and pkg_resources.evaluate_marker(key[1:]):
|
||||
install_requires.extend(value)
|
||||
except Exception:
|
||||
logging.getLogger(__name__).exception(
|
||||
'Something went wrong calculating platform specific dependencies, so '
|
||||
"you're getting them all!"
|
||||
)
|
||||
for key, value in extras_require.items():
|
||||
if key.startswith(':'):
|
||||
install_requires.extend(value)
|
||||
|
||||
|
||||
setup(
|
||||
name='vcrpy',
|
||||
version='1.4.0',
|
||||
version='1.7.4',
|
||||
description=(
|
||||
"Automatically mock your HTTP interactions to simplify and "
|
||||
"speed up testing"
|
||||
),
|
||||
long_description=long_description,
|
||||
author='Kevin McCarthy',
|
||||
author_email='me@kevinmccarthy.org',
|
||||
url='https://github.com/kevin1024/vcrpy',
|
||||
packages=find_packages(exclude=("tests*",)),
|
||||
install_requires=['PyYAML', 'mock', 'six', 'contextlib2',
|
||||
'wrapt', 'backport_collections'],
|
||||
install_requires=install_requires,
|
||||
extras_require=extras_require,
|
||||
license='MIT',
|
||||
tests_require=['pytest', 'mock', 'pytest-localserver'],
|
||||
cmdclass={'test': PyTest},
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import pytest
|
||||
boto = pytest.importorskip("boto")
|
||||
|
||||
import boto
|
||||
import boto.iam
|
||||
from boto.s3.connection import S3Connection
|
||||
@@ -7,6 +8,7 @@ from boto.s3.key import Key
|
||||
from ConfigParser import DuplicateSectionError
|
||||
import vcr
|
||||
|
||||
|
||||
def test_boto_stubs(tmpdir):
|
||||
with vcr.use_cassette(str(tmpdir.join('boto-stubs.yml'))):
|
||||
# Perform the imports within the patched context so that
|
||||
@@ -17,16 +19,18 @@ def test_boto_stubs(tmpdir):
|
||||
assert issubclass(CertValidatingHTTPSConnection, VCRCertValidatingHTTPSConnection)
|
||||
CertValidatingHTTPSConnection('hostname.does.not.matter')
|
||||
|
||||
|
||||
def test_boto_without_vcr():
|
||||
s3_conn = S3Connection()
|
||||
s3_bucket = s3_conn.get_bucket('boto-demo-1394171994') # a bucket you can access
|
||||
s3_bucket = s3_conn.get_bucket('boto-demo-1394171994') # a bucket you can access
|
||||
k = Key(s3_bucket)
|
||||
k.key = 'test.txt'
|
||||
k.set_contents_from_string('hello world i am a string')
|
||||
|
||||
|
||||
def test_boto_medium_difficulty(tmpdir):
|
||||
s3_conn = S3Connection()
|
||||
s3_bucket = s3_conn.get_bucket('boto-demo-1394171994') # a bucket you can access
|
||||
s3_bucket = s3_conn.get_bucket('boto-demo-1394171994') # a bucket you can access
|
||||
with vcr.use_cassette(str(tmpdir.join('boto-medium.yml'))) as cass:
|
||||
k = Key(s3_bucket)
|
||||
k.key = 'test.txt'
|
||||
@@ -41,18 +45,19 @@ def test_boto_medium_difficulty(tmpdir):
|
||||
def test_boto_hardcore_mode(tmpdir):
|
||||
with vcr.use_cassette(str(tmpdir.join('boto-hardcore.yml'))) as cass:
|
||||
s3_conn = S3Connection()
|
||||
s3_bucket = s3_conn.get_bucket('boto-demo-1394171994') # a bucket you can access
|
||||
s3_bucket = s3_conn.get_bucket('boto-demo-1394171994') # a bucket you can access
|
||||
k = Key(s3_bucket)
|
||||
k.key = 'test.txt'
|
||||
k.set_contents_from_string('hello world i am a string')
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join('boto-hardcore.yml'))) as cass:
|
||||
s3_conn = S3Connection()
|
||||
s3_bucket = s3_conn.get_bucket('boto-demo-1394171994') # a bucket you can access
|
||||
s3_bucket = s3_conn.get_bucket('boto-demo-1394171994') # a bucket you can access
|
||||
k = Key(s3_bucket)
|
||||
k.key = 'test.txt'
|
||||
k.set_contents_from_string('hello world i am a string')
|
||||
|
||||
|
||||
def test_boto_iam(tmpdir):
|
||||
try:
|
||||
boto.config.add_section('Boto')
|
||||
|
||||
@@ -4,6 +4,7 @@ from six.moves.urllib.request import urlopen, Request
|
||||
from six.moves.urllib.parse import urlencode
|
||||
from six.moves.urllib.error import HTTPError
|
||||
import vcr
|
||||
import json
|
||||
|
||||
|
||||
def _request_with_auth(url, username, password):
|
||||
@@ -16,11 +17,7 @@ def _request_with_auth(url, username, password):
|
||||
|
||||
|
||||
def _find_header(cassette, header):
|
||||
for request in cassette.requests:
|
||||
for k in request.headers:
|
||||
if header.lower() == k.lower():
|
||||
return True
|
||||
return False
|
||||
return any(header in request.headers for request in cassette.requests)
|
||||
|
||||
|
||||
def test_filter_basic_auth(tmpdir):
|
||||
@@ -66,6 +63,18 @@ def test_filter_post_data(tmpdir):
|
||||
assert b'id=secret' not in cass.requests[0].body
|
||||
|
||||
|
||||
def test_filter_json_post_data(tmpdir):
|
||||
data = json.dumps({'id': 'secret', 'foo': 'bar'}).encode('utf-8')
|
||||
request = Request('http://httpbin.org/post', data=data)
|
||||
request.add_header('Content-Type', 'application/json')
|
||||
|
||||
cass_file = str(tmpdir.join('filter_jpd.yaml'))
|
||||
with vcr.use_cassette(cass_file, filter_post_data_parameters=['id']):
|
||||
urlopen(request)
|
||||
with vcr.use_cassette(cass_file, filter_post_data_parameters=['id']) as cass:
|
||||
assert b'"id": "secret"' not in cass.requests[0].body
|
||||
|
||||
|
||||
def test_filter_callback(tmpdir):
|
||||
url = 'http://httpbin.org/get'
|
||||
cass_file = str(tmpdir.join('basic_auth_filter.yaml'))
|
||||
|
||||
@@ -56,6 +56,17 @@ def test_response_headers(scheme, tmpdir):
|
||||
resp, _ = httplib2.Http().request(url)
|
||||
assert set(headers) == set(resp.items())
|
||||
|
||||
def test_effective_url(scheme, tmpdir):
|
||||
'''Ensure that the effective_url is captured'''
|
||||
url = scheme + '://httpbin.org/redirect-to?url=/html'
|
||||
with vcr.use_cassette(str(tmpdir.join('headers.yaml'))) as cass:
|
||||
resp, _ = httplib2.Http().request(url)
|
||||
effective_url = resp['content-location']
|
||||
assert effective_url == scheme + '://httpbin.org/html'
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join('headers.yaml'))) as cass:
|
||||
resp, _ = httplib2.Http().request(url)
|
||||
assert effective_url == resp['content-location']
|
||||
|
||||
def test_multiple_requests(scheme, tmpdir):
|
||||
'''Ensure that we can cache multiple requests'''
|
||||
|
||||
@@ -44,6 +44,15 @@ def test_body(tmpdir, scheme):
|
||||
with vcr.use_cassette(str(tmpdir.join('body.yaml'))):
|
||||
assert content == requests.get(url).content
|
||||
|
||||
def test_effective_url(scheme, tmpdir):
|
||||
'''Ensure that the effective_url is captured'''
|
||||
url = scheme + '://httpbin.org/redirect-to?url=/html'
|
||||
with vcr.use_cassette(str(tmpdir.join('url.yaml'))):
|
||||
effective_url = requests.get(url).url
|
||||
assert effective_url == scheme + '://httpbin.org/html'
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join('url.yaml'))):
|
||||
assert effective_url == requests.get(url).url
|
||||
|
||||
def test_auth(tmpdir, scheme):
|
||||
'''Ensure that we can handle basic auth'''
|
||||
@@ -217,3 +226,20 @@ def test_post_file(tmpdir, scheme):
|
||||
with open('tox.ini', 'rb') as f:
|
||||
new_response = requests.post(url, f).content
|
||||
assert original_response == new_response
|
||||
|
||||
|
||||
def test_filter_post_params(tmpdir, scheme):
|
||||
'''
|
||||
This tests the issue in https://github.com/kevin1024/vcrpy/issues/158
|
||||
|
||||
Ensure that a post request made through requests can still be filtered.
|
||||
with vcr.use_cassette(cass_file, filter_post_data_parameters=['id']) as cass:
|
||||
assert b'id=secret' not in cass.requests[0].body
|
||||
'''
|
||||
url = scheme + '://httpbin.org/post'
|
||||
cass_loc = str(tmpdir.join('filter_post_params.yaml'))
|
||||
with vcr.use_cassette(cass_loc, filter_post_data_parameters=['key']) as cass:
|
||||
requests.post(url, data={'key': 'value'})
|
||||
with vcr.use_cassette(cass_loc, filter_post_data_parameters=['key']) as cass:
|
||||
assert b'key=value' not in cass.requests[0].body
|
||||
|
||||
|
||||
355
tests/integration/test_tornado.py
Normal file
355
tests/integration/test_tornado.py
Normal file
@@ -0,0 +1,355 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
'''Test requests' interaction with vcr'''
|
||||
|
||||
import json
|
||||
|
||||
import pytest
|
||||
import vcr
|
||||
from vcr.errors import CannotOverwriteExistingCassetteException
|
||||
|
||||
from assertions import assert_cassette_empty, assert_is_json
|
||||
|
||||
|
||||
http = pytest.importorskip("tornado.httpclient")
|
||||
|
||||
|
||||
@pytest.fixture(params=['simple', 'curl', 'default'])
|
||||
def get_client(request):
|
||||
if request.param == 'simple':
|
||||
from tornado import simple_httpclient as simple
|
||||
return (lambda: simple.SimpleAsyncHTTPClient())
|
||||
elif request.param == 'curl':
|
||||
curl = pytest.importorskip("tornado.curl_httpclient")
|
||||
return (lambda: curl.CurlAsyncHTTPClient())
|
||||
else:
|
||||
return (lambda: http.AsyncHTTPClient())
|
||||
|
||||
|
||||
def get(client, url, **kwargs):
|
||||
raise_error = kwargs.pop('raise_error', True)
|
||||
return client.fetch(
|
||||
http.HTTPRequest(url, method='GET', **kwargs),
|
||||
raise_error=raise_error,
|
||||
)
|
||||
|
||||
|
||||
def post(client, url, data=None, **kwargs):
|
||||
if data:
|
||||
kwargs['body'] = json.dumps(data)
|
||||
return client.fetch(http.HTTPRequest(url, method='POST', **kwargs))
|
||||
|
||||
|
||||
@pytest.fixture(params=["https", "http"])
|
||||
def scheme(request):
|
||||
'''Fixture that returns both http and https.'''
|
||||
return request.param
|
||||
|
||||
|
||||
@pytest.mark.gen_test
|
||||
def test_status_code(get_client, scheme, tmpdir):
|
||||
'''Ensure that we can read the status code'''
|
||||
url = scheme + '://httpbin.org/'
|
||||
with vcr.use_cassette(str(tmpdir.join('atts.yaml'))):
|
||||
status_code = (yield get(get_client(), url)).code
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join('atts.yaml'))) as cass:
|
||||
assert status_code == (yield get(get_client(), url)).code
|
||||
assert 1 == cass.play_count
|
||||
|
||||
|
||||
@pytest.mark.gen_test
|
||||
def test_headers(get_client, scheme, tmpdir):
|
||||
'''Ensure that we can read the headers back'''
|
||||
url = scheme + '://httpbin.org/'
|
||||
with vcr.use_cassette(str(tmpdir.join('headers.yaml'))):
|
||||
headers = (yield get(get_client(), url)).headers
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join('headers.yaml'))) as cass:
|
||||
assert headers == (yield get(get_client(), url)).headers
|
||||
assert 1 == cass.play_count
|
||||
|
||||
|
||||
@pytest.mark.gen_test
|
||||
def test_body(get_client, tmpdir, scheme):
|
||||
'''Ensure the responses are all identical enough'''
|
||||
|
||||
url = scheme + '://httpbin.org/bytes/1024'
|
||||
with vcr.use_cassette(str(tmpdir.join('body.yaml'))):
|
||||
content = (yield get(get_client(), url)).body
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join('body.yaml'))) as cass:
|
||||
assert content == (yield get(get_client(), url)).body
|
||||
assert 1 == cass.play_count
|
||||
|
||||
@pytest.mark.gen_test
|
||||
def test_effective_url(get_client, scheme, tmpdir):
|
||||
'''Ensure that the effective_url is captured'''
|
||||
url = scheme + '://httpbin.org/redirect-to?url=/html'
|
||||
with vcr.use_cassette(str(tmpdir.join('url.yaml'))):
|
||||
effective_url = (yield get(get_client(), url)).effective_url
|
||||
assert effective_url == scheme + '://httpbin.org/html'
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join('url.yaml'))) as cass:
|
||||
assert effective_url == (yield get(get_client(), url)).effective_url
|
||||
assert 1 == cass.play_count
|
||||
|
||||
@pytest.mark.gen_test
|
||||
def test_auth(get_client, tmpdir, scheme):
|
||||
'''Ensure that we can handle basic auth'''
|
||||
auth = ('user', 'passwd')
|
||||
url = scheme + '://httpbin.org/basic-auth/user/passwd'
|
||||
with vcr.use_cassette(str(tmpdir.join('auth.yaml'))):
|
||||
one = yield get(
|
||||
get_client(), url, auth_username=auth[0], auth_password=auth[1]
|
||||
)
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join('auth.yaml'))) as cass:
|
||||
two = yield get(
|
||||
get_client(), url, auth_username=auth[0], auth_password=auth[1]
|
||||
)
|
||||
assert one.body == two.body
|
||||
assert one.code == two.code
|
||||
assert 1 == cass.play_count
|
||||
|
||||
|
||||
@pytest.mark.gen_test
|
||||
def test_auth_failed(get_client, tmpdir, scheme):
|
||||
'''Ensure that we can save failed auth statuses'''
|
||||
auth = ('user', 'wrongwrongwrong')
|
||||
url = scheme + '://httpbin.org/basic-auth/user/passwd'
|
||||
with vcr.use_cassette(str(tmpdir.join('auth-failed.yaml'))) as cass:
|
||||
# Ensure that this is empty to begin with
|
||||
assert_cassette_empty(cass)
|
||||
one = yield get(
|
||||
get_client(),
|
||||
url,
|
||||
auth_username=auth[0],
|
||||
auth_password=auth[1],
|
||||
raise_error=False
|
||||
)
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join('auth-failed.yaml'))) as cass:
|
||||
two = yield get(
|
||||
get_client(),
|
||||
url,
|
||||
auth_username=auth[0],
|
||||
auth_password=auth[1],
|
||||
raise_error=False
|
||||
)
|
||||
assert one.body == two.body
|
||||
assert one.code == two.code == 401
|
||||
assert 1 == cass.play_count
|
||||
|
||||
|
||||
@pytest.mark.gen_test
|
||||
def test_post(get_client, tmpdir, scheme):
|
||||
'''Ensure that we can post and cache the results'''
|
||||
data = {'key1': 'value1', 'key2': 'value2'}
|
||||
url = scheme + '://httpbin.org/post'
|
||||
with vcr.use_cassette(str(tmpdir.join('requests.yaml'))):
|
||||
req1 = (yield post(get_client(), url, data)).body
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join('requests.yaml'))) as cass:
|
||||
req2 = (yield post(get_client(), url, data)).body
|
||||
|
||||
assert req1 == req2
|
||||
assert 1 == cass.play_count
|
||||
|
||||
|
||||
@pytest.mark.gen_test
|
||||
def test_redirects(get_client, tmpdir, scheme):
|
||||
'''Ensure that we can handle redirects'''
|
||||
url = scheme + '://httpbin.org/redirect-to?url=bytes/1024'
|
||||
with vcr.use_cassette(str(tmpdir.join('requests.yaml'))):
|
||||
content = (yield get(get_client(), url)).body
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join('requests.yaml'))) as cass:
|
||||
assert content == (yield get(get_client(), url)).body
|
||||
assert cass.play_count == 1
|
||||
|
||||
|
||||
@pytest.mark.gen_test
|
||||
def test_cross_scheme(get_client, tmpdir, scheme):
|
||||
'''Ensure that requests between schemes are treated separately'''
|
||||
# First fetch a url under http, and then again under https and then
|
||||
# ensure that we haven't served anything out of cache, and we have two
|
||||
# requests / response pairs in the cassette
|
||||
with vcr.use_cassette(str(tmpdir.join('cross_scheme.yaml'))) as cass:
|
||||
yield get(get_client(), 'https://httpbin.org/')
|
||||
yield get(get_client(), 'http://httpbin.org/')
|
||||
assert cass.play_count == 0
|
||||
assert len(cass) == 2
|
||||
|
||||
# Then repeat the same requests and ensure both were replayed.
|
||||
with vcr.use_cassette(str(tmpdir.join('cross_scheme.yaml'))) as cass:
|
||||
yield get(get_client(), 'https://httpbin.org/')
|
||||
yield get(get_client(), 'http://httpbin.org/')
|
||||
assert cass.play_count == 2
|
||||
|
||||
|
||||
@pytest.mark.gen_test
|
||||
def test_gzip(get_client, tmpdir, scheme):
|
||||
'''
|
||||
Ensure that httpclient is able to automatically decompress the response
|
||||
body
|
||||
'''
|
||||
url = scheme + '://httpbin.org/gzip'
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join('gzip.yaml'))):
|
||||
response = yield get(get_client(), url, decompress_response=True)
|
||||
assert_is_json(response.body)
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join('gzip.yaml'))) as cass:
|
||||
response = yield get(get_client(), url, decompress_response=True)
|
||||
assert_is_json(response.body)
|
||||
assert 1 == cass.play_count
|
||||
|
||||
|
||||
@pytest.mark.gen_test
|
||||
def test_https_with_cert_validation_disabled(get_client, tmpdir):
|
||||
cass_path = str(tmpdir.join('cert_validation_disabled.yaml'))
|
||||
|
||||
with vcr.use_cassette(cass_path):
|
||||
yield get(get_client(), 'https://httpbin.org', validate_cert=False)
|
||||
|
||||
with vcr.use_cassette(cass_path) as cass:
|
||||
yield get(get_client(), 'https://httpbin.org', validate_cert=False)
|
||||
assert 1 == cass.play_count
|
||||
|
||||
|
||||
@pytest.mark.gen_test
|
||||
def test_unsupported_features_raises_in_future(get_client, tmpdir):
|
||||
'''Ensure that the exception for an AsyncHTTPClient feature not being
|
||||
supported is raised inside the future.'''
|
||||
|
||||
def callback(chunk):
|
||||
assert False, "Did not expect to be called."
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join('invalid.yaml'))):
|
||||
future = get(
|
||||
get_client(), 'http://httpbin.org', streaming_callback=callback
|
||||
)
|
||||
|
||||
with pytest.raises(Exception) as excinfo:
|
||||
yield future
|
||||
|
||||
assert "not yet supported by VCR" in str(excinfo)
|
||||
|
||||
|
||||
@pytest.mark.gen_test
|
||||
def test_unsupported_features_raise_error_disabled(get_client, tmpdir):
|
||||
'''Ensure that the exception for an AsyncHTTPClient feature not being
|
||||
supported is not raised if raise_error=False.'''
|
||||
|
||||
def callback(chunk):
|
||||
assert False, "Did not expect to be called."
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join('invalid.yaml'))):
|
||||
response = yield get(
|
||||
get_client(),
|
||||
'http://httpbin.org',
|
||||
streaming_callback=callback,
|
||||
raise_error=False,
|
||||
)
|
||||
|
||||
assert "not yet supported by VCR" in str(response.error)
|
||||
|
||||
|
||||
@pytest.mark.gen_test
|
||||
def test_cannot_overwrite_cassette_raises_in_future(get_client, tmpdir):
|
||||
'''Ensure that CannotOverwriteExistingCassetteException is raised inside
|
||||
the future.'''
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join('overwrite.yaml'))):
|
||||
yield get(get_client(), 'http://httpbin.org/get')
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join('overwrite.yaml'))):
|
||||
future = get(get_client(), 'http://httpbin.org/headers')
|
||||
|
||||
with pytest.raises(CannotOverwriteExistingCassetteException):
|
||||
yield future
|
||||
|
||||
|
||||
@pytest.mark.gen_test
|
||||
def test_cannot_overwrite_cassette_raise_error_disabled(get_client, tmpdir):
|
||||
'''Ensure that CannotOverwriteExistingCassetteException is not raised if
|
||||
raise_error=False in the fetch() call.'''
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join('overwrite.yaml'))):
|
||||
yield get(
|
||||
get_client(), 'http://httpbin.org/get', raise_error=False
|
||||
)
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join('overwrite.yaml'))):
|
||||
response = yield get(
|
||||
get_client(), 'http://httpbin.org/headers', raise_error=False
|
||||
)
|
||||
|
||||
assert isinstance(response.error, CannotOverwriteExistingCassetteException)
|
||||
|
||||
|
||||
@pytest.mark.gen_test
|
||||
@vcr.use_cassette(path_transformer=vcr.default_vcr.ensure_suffix('.yaml'))
|
||||
def test_tornado_with_decorator_use_cassette(get_client):
|
||||
response = yield get_client().fetch(
|
||||
http.HTTPRequest('http://www.google.com/', method='GET')
|
||||
)
|
||||
assert response.body.decode('utf-8') == "not actually google"
|
||||
|
||||
|
||||
@pytest.mark.gen_test
|
||||
@vcr.use_cassette(path_transformer=vcr.default_vcr.ensure_suffix('.yaml'))
|
||||
def test_tornado_exception_can_be_caught(get_client):
|
||||
try:
|
||||
yield get(get_client(), 'http://httpbin.org/status/500')
|
||||
except http.HTTPError as e:
|
||||
assert e.code == 500
|
||||
|
||||
try:
|
||||
yield get(get_client(), 'http://httpbin.org/status/404')
|
||||
except http.HTTPError as e:
|
||||
assert e.code == 404
|
||||
|
||||
|
||||
@pytest.mark.gen_test
|
||||
def test_existing_references_get_patched(tmpdir):
|
||||
from tornado.httpclient import AsyncHTTPClient
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join('data.yaml'))):
|
||||
client = AsyncHTTPClient()
|
||||
yield get(client, 'http://httpbin.org/get')
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join('data.yaml'))) as cass:
|
||||
yield get(client, 'http://httpbin.org/get')
|
||||
assert cass.play_count == 1
|
||||
|
||||
|
||||
@pytest.mark.gen_test
|
||||
def test_existing_instances_get_patched(get_client, tmpdir):
|
||||
'''Ensure that existing instances of AsyncHTTPClient get patched upon
|
||||
entering VCR context.'''
|
||||
|
||||
client = get_client()
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join('data.yaml'))):
|
||||
yield get(client, 'http://httpbin.org/get')
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join('data.yaml'))) as cass:
|
||||
yield get(client, 'http://httpbin.org/get')
|
||||
assert cass.play_count == 1
|
||||
|
||||
|
||||
@pytest.mark.gen_test
|
||||
def test_request_time_is_set(get_client, tmpdir):
|
||||
'''Ensures that the request_time on HTTPResponses is set.'''
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join('data.yaml'))):
|
||||
client = get_client()
|
||||
response = yield get(client, 'http://httpbin.org/get')
|
||||
assert response.request_time is not None
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join('data.yaml'))) as cass:
|
||||
client = get_client()
|
||||
response = yield get(client, 'http://httpbin.org/get')
|
||||
assert response.request_time is not None
|
||||
assert cass.play_count == 1
|
||||
62
tests/integration/test_tornado_exception_can_be_caught.yaml
Normal file
62
tests/integration/test_tornado_exception_can_be_caught.yaml
Normal file
@@ -0,0 +1,62 @@
|
||||
interactions:
|
||||
- request:
|
||||
body: null
|
||||
headers: {}
|
||||
method: GET
|
||||
uri: http://httpbin.org/status/500
|
||||
response:
|
||||
body: {string: !!python/unicode ''}
|
||||
headers:
|
||||
- !!python/tuple
|
||||
- Content-Length
|
||||
- ['0']
|
||||
- !!python/tuple
|
||||
- Server
|
||||
- [nginx]
|
||||
- !!python/tuple
|
||||
- Connection
|
||||
- [close]
|
||||
- !!python/tuple
|
||||
- Access-Control-Allow-Credentials
|
||||
- ['true']
|
||||
- !!python/tuple
|
||||
- Date
|
||||
- ['Thu, 30 Jul 2015 17:32:39 GMT']
|
||||
- !!python/tuple
|
||||
- Access-Control-Allow-Origin
|
||||
- ['*']
|
||||
- !!python/tuple
|
||||
- Content-Type
|
||||
- [text/html; charset=utf-8]
|
||||
status: {code: 500, message: INTERNAL SERVER ERROR}
|
||||
- request:
|
||||
body: null
|
||||
headers: {}
|
||||
method: GET
|
||||
uri: http://httpbin.org/status/404
|
||||
response:
|
||||
body: {string: !!python/unicode ''}
|
||||
headers:
|
||||
- !!python/tuple
|
||||
- Content-Length
|
||||
- ['0']
|
||||
- !!python/tuple
|
||||
- Server
|
||||
- [nginx]
|
||||
- !!python/tuple
|
||||
- Connection
|
||||
- [close]
|
||||
- !!python/tuple
|
||||
- Access-Control-Allow-Credentials
|
||||
- ['true']
|
||||
- !!python/tuple
|
||||
- Date
|
||||
- ['Thu, 30 Jul 2015 17:32:39 GMT']
|
||||
- !!python/tuple
|
||||
- Access-Control-Allow-Origin
|
||||
- ['*']
|
||||
- !!python/tuple
|
||||
- Content-Type
|
||||
- [text/html; charset=utf-8]
|
||||
status: {code: 404, message: NOT FOUND}
|
||||
version: 1
|
||||
@@ -0,0 +1,53 @@
|
||||
interactions:
|
||||
- request:
|
||||
body: null
|
||||
headers: {}
|
||||
method: GET
|
||||
uri: http://www.google.com/
|
||||
response:
|
||||
body: {string: !!python/unicode 'not actually google'}
|
||||
headers:
|
||||
- !!python/tuple
|
||||
- Expires
|
||||
- ['-1']
|
||||
- !!python/tuple
|
||||
- Connection
|
||||
- [close]
|
||||
- !!python/tuple
|
||||
- P3p
|
||||
- ['CP="This is not a P3P policy! See http://www.google.com/support/accounts/bin/answer.py?hl=en&answer=151657
|
||||
for more info."']
|
||||
- !!python/tuple
|
||||
- Alternate-Protocol
|
||||
- ['80:quic,p=0']
|
||||
- !!python/tuple
|
||||
- Accept-Ranges
|
||||
- [none]
|
||||
- !!python/tuple
|
||||
- X-Xss-Protection
|
||||
- [1; mode=block]
|
||||
- !!python/tuple
|
||||
- Vary
|
||||
- [Accept-Encoding]
|
||||
- !!python/tuple
|
||||
- Date
|
||||
- ['Thu, 30 Jul 2015 08:41:40 GMT']
|
||||
- !!python/tuple
|
||||
- Cache-Control
|
||||
- ['private, max-age=0']
|
||||
- !!python/tuple
|
||||
- Content-Type
|
||||
- [text/html; charset=ISO-8859-1]
|
||||
- !!python/tuple
|
||||
- Set-Cookie
|
||||
- ['PREF=ID=1111111111111111:FF=0:TM=1438245700:LM=1438245700:V=1:S=GAzVO0ALebSpC_cJ;
|
||||
expires=Sat, 29-Jul-2017 08:41:40 GMT; path=/; domain=.google.com', 'NID=69=Br7oRAwgmKoK__HC6FEnuxglTFDmFxqP6Md63lKhzW1w6WkDbp3U90CDxnUKvDP6wJH8yxY5Lk5ZnFf66Q1B0d4OsYoKgq0vjfBAYXuCIAWtOuGZEOsFXanXs7pt2Mjx;
|
||||
expires=Fri, 29-Jan-2016 08:41:40 GMT; path=/; domain=.google.com; HttpOnly']
|
||||
- !!python/tuple
|
||||
- X-Frame-Options
|
||||
- [SAMEORIGIN]
|
||||
- !!python/tuple
|
||||
- Server
|
||||
- [gws]
|
||||
status: {code: 200, message: OK}
|
||||
version: 1
|
||||
@@ -49,6 +49,15 @@ def test_response_headers(scheme, tmpdir):
|
||||
open2 = urlopen(url).info().items()
|
||||
assert sorted(open1) == sorted(open2)
|
||||
|
||||
def test_effective_url(scheme, tmpdir):
|
||||
'''Ensure that the effective_url is captured'''
|
||||
url = scheme + '://httpbin.org/redirect-to?url=/html'
|
||||
with vcr.use_cassette(str(tmpdir.join('headers.yaml'))) as cass:
|
||||
effective_url = urlopen(url).geturl()
|
||||
assert effective_url == scheme + '://httpbin.org/html'
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join('headers.yaml'))) as cass:
|
||||
assert effective_url == urlopen(url).geturl()
|
||||
|
||||
def test_multiple_requests(scheme, tmpdir):
|
||||
'''Ensure that we can cache multiple requests'''
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import pytest
|
||||
from six.moves import xmlrpc_client
|
||||
|
||||
requests = pytest.importorskip("requests")
|
||||
|
||||
import vcr
|
||||
@@ -64,3 +66,24 @@ def test_cookies(tmpdir):
|
||||
r1 = s.get("http://httpbin.org/cookies/set?k1=v1&k2=v2")
|
||||
r2 = s.get("http://httpbin.org/cookies")
|
||||
assert len(r2.json()['cookies']) == 2
|
||||
|
||||
|
||||
def test_amazon_doctype(tmpdir):
|
||||
# amazon gzips its homepage. For some reason, in requests 2.7, it's not
|
||||
# getting gunzipped.
|
||||
with vcr.use_cassette(str(tmpdir.join('amz.yml'))):
|
||||
r = requests.get('http://www.amazon.com')
|
||||
assert 'html' in r.text
|
||||
|
||||
|
||||
def test_xmlrpclib(tmpdir):
|
||||
with vcr.use_cassette(str(tmpdir.join('xmlrpcvideo.yaml'))):
|
||||
roundup_server = xmlrpc_client.ServerProxy('http://bugs.python.org/xmlrpc', allow_none=True)
|
||||
original_schema = roundup_server.schema()
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join('xmlrpcvideo.yaml'))) as cassette:
|
||||
roundup_server = xmlrpc_client.ServerProxy('http://bugs.python.org/xmlrpc', allow_none=True)
|
||||
second_schema = roundup_server.schema()
|
||||
|
||||
assert original_schema == second_schema
|
||||
|
||||
|
||||
@@ -1,25 +1,26 @@
|
||||
import copy
|
||||
import inspect
|
||||
import os
|
||||
|
||||
from six.moves import http_client as httplib
|
||||
import contextlib2
|
||||
import mock
|
||||
import pytest
|
||||
import yaml
|
||||
|
||||
from vcr.compat import mock, contextlib
|
||||
from vcr.cassette import Cassette
|
||||
from vcr.errors import UnhandledHTTPRequestError
|
||||
from vcr.patch import force_reset
|
||||
from vcr.matchers import path, method, query, host
|
||||
from vcr.stubs import VCRHTTPSConnection
|
||||
|
||||
|
||||
|
||||
def test_cassette_load(tmpdir):
|
||||
a_file = tmpdir.join('test_cassette.yml')
|
||||
a_file.write(yaml.dump({'interactions': [
|
||||
{'request': {'body': '', 'uri': 'foo', 'method': 'GET', 'headers': {}},
|
||||
'response': 'bar'}
|
||||
]}))
|
||||
a_cassette = Cassette.load(str(a_file))
|
||||
a_cassette = Cassette.load(path=str(a_file))
|
||||
assert len(a_cassette) == 1
|
||||
|
||||
|
||||
@@ -87,33 +88,35 @@ def make_get_request():
|
||||
@mock.patch('vcr.cassette.Cassette.can_play_response_for', return_value=True)
|
||||
@mock.patch('vcr.stubs.VCRHTTPResponse')
|
||||
def test_function_decorated_with_use_cassette_can_be_invoked_multiple_times(*args):
|
||||
decorated_function = Cassette.use('test')(make_get_request)
|
||||
for i in range(2):
|
||||
decorated_function = Cassette.use(path='test')(make_get_request)
|
||||
for i in range(4):
|
||||
decorated_function()
|
||||
|
||||
|
||||
def test_arg_getter_functionality():
|
||||
arg_getter = mock.Mock(return_value=('test', {}))
|
||||
arg_getter = mock.Mock(return_value={'path': 'test'})
|
||||
context_decorator = Cassette.use_arg_getter(arg_getter)
|
||||
|
||||
with context_decorator as cassette:
|
||||
assert cassette._path == 'test'
|
||||
|
||||
arg_getter.return_value = ('other', {})
|
||||
arg_getter.return_value = {'path': 'other'}
|
||||
|
||||
with context_decorator as cassette:
|
||||
assert cassette._path == 'other'
|
||||
|
||||
arg_getter.return_value = ('', {'filter_headers': ('header_name',)})
|
||||
arg_getter.return_value = {'path': 'other', 'filter_headers': ('header_name',)}
|
||||
|
||||
@context_decorator
|
||||
def function():
|
||||
pass
|
||||
|
||||
with mock.patch.object(Cassette, 'load', return_value=mock.MagicMock(inject=False)) as cassette_load:
|
||||
with mock.patch.object(
|
||||
Cassette, 'load',
|
||||
return_value=mock.MagicMock(inject=False)
|
||||
) as cassette_load:
|
||||
function()
|
||||
cassette_load.assert_called_once_with(arg_getter.return_value[0],
|
||||
**arg_getter.return_value[1])
|
||||
cassette_load.assert_called_once_with(**arg_getter.return_value)
|
||||
|
||||
|
||||
def test_cassette_not_all_played():
|
||||
@@ -155,14 +158,14 @@ def test_nesting_cassette_context_managers(*args):
|
||||
second_response = copy.deepcopy(first_response)
|
||||
second_response['body']['string'] = b'second_response'
|
||||
|
||||
with contextlib2.ExitStack() as exit_stack:
|
||||
first_cassette = exit_stack.enter_context(Cassette.use('test'))
|
||||
with contextlib.ExitStack() as exit_stack:
|
||||
first_cassette = exit_stack.enter_context(Cassette.use(path='test'))
|
||||
exit_stack.enter_context(mock.patch.object(first_cassette, 'play_response',
|
||||
return_value=first_response))
|
||||
assert_get_response_body_is('first_response')
|
||||
|
||||
# Make sure a second cassette can supercede the first
|
||||
with Cassette.use('test') as second_cassette:
|
||||
with Cassette.use(path='test') as second_cassette:
|
||||
with mock.patch.object(second_cassette, 'play_response', return_value=second_response):
|
||||
assert_get_response_body_is('second_response')
|
||||
|
||||
@@ -172,12 +175,12 @@ def test_nesting_cassette_context_managers(*args):
|
||||
|
||||
def test_nesting_context_managers_by_checking_references_of_http_connection():
|
||||
original = httplib.HTTPConnection
|
||||
with Cassette.use('test'):
|
||||
with Cassette.use(path='test'):
|
||||
first_cassette_HTTPConnection = httplib.HTTPConnection
|
||||
with Cassette.use('test'):
|
||||
with Cassette.use(path='test'):
|
||||
second_cassette_HTTPConnection = httplib.HTTPConnection
|
||||
assert second_cassette_HTTPConnection is not first_cassette_HTTPConnection
|
||||
with Cassette.use('test'):
|
||||
with Cassette.use(path='test'):
|
||||
assert httplib.HTTPConnection is not second_cassette_HTTPConnection
|
||||
with force_reset():
|
||||
assert httplib.HTTPConnection is original
|
||||
@@ -188,12 +191,14 @@ def test_nesting_context_managers_by_checking_references_of_http_connection():
|
||||
def test_custom_patchers():
|
||||
class Test(object):
|
||||
attribute = None
|
||||
with Cassette.use('custom_patches', custom_patches=((Test, 'attribute', VCRHTTPSConnection),)):
|
||||
with Cassette.use(path='custom_patches',
|
||||
custom_patches=((Test, 'attribute', VCRHTTPSConnection),)):
|
||||
assert issubclass(Test.attribute, VCRHTTPSConnection)
|
||||
assert VCRHTTPSConnection is not Test.attribute
|
||||
old_attribute = Test.attribute
|
||||
|
||||
with Cassette.use('custom_patches', custom_patches=((Test, 'attribute', VCRHTTPSConnection),)):
|
||||
with Cassette.use(path='custom_patches',
|
||||
custom_patches=((Test, 'attribute', VCRHTTPSConnection),)):
|
||||
assert issubclass(Test.attribute, VCRHTTPSConnection)
|
||||
assert VCRHTTPSConnection is not Test.attribute
|
||||
assert Test.attribute is not old_attribute
|
||||
@@ -201,3 +206,107 @@ def test_custom_patchers():
|
||||
assert issubclass(Test.attribute, VCRHTTPSConnection)
|
||||
assert VCRHTTPSConnection is not Test.attribute
|
||||
assert Test.attribute is old_attribute
|
||||
|
||||
|
||||
def test_decorated_functions_are_reentrant():
|
||||
info = {"second": False}
|
||||
original_conn = httplib.HTTPConnection
|
||||
@Cassette.use(path='whatever', inject=True)
|
||||
def test_function(cassette):
|
||||
if info['second']:
|
||||
assert httplib.HTTPConnection is not info['first_conn']
|
||||
else:
|
||||
info['first_conn'] = httplib.HTTPConnection
|
||||
info['second'] = True
|
||||
test_function()
|
||||
assert httplib.HTTPConnection is info['first_conn']
|
||||
test_function()
|
||||
assert httplib.HTTPConnection is original_conn
|
||||
|
||||
|
||||
def test_cassette_use_called_without_path_uses_function_to_generate_path():
|
||||
@Cassette.use(inject=True)
|
||||
def function_name(cassette):
|
||||
assert cassette._path == 'function_name'
|
||||
function_name()
|
||||
|
||||
|
||||
def test_path_transformer_with_function_path():
|
||||
path_transformer = lambda path: os.path.join('a', path)
|
||||
@Cassette.use(inject=True, path_transformer=path_transformer)
|
||||
def function_name(cassette):
|
||||
assert cassette._path == os.path.join('a', 'function_name')
|
||||
function_name()
|
||||
|
||||
|
||||
def test_path_transformer_with_context_manager():
|
||||
with Cassette.use(
|
||||
path='b', path_transformer=lambda *args: 'a'
|
||||
) as cassette:
|
||||
assert cassette._path == 'a'
|
||||
|
||||
|
||||
def test_path_transformer_None():
|
||||
with Cassette.use(
|
||||
path='a', path_transformer=None,
|
||||
) as cassette:
|
||||
assert cassette._path == 'a'
|
||||
|
||||
|
||||
def test_func_path_generator():
|
||||
def generator(function):
|
||||
return os.path.join(os.path.dirname(inspect.getfile(function)),
|
||||
function.__name__)
|
||||
@Cassette.use(inject=True, func_path_generator=generator)
|
||||
def function_name(cassette):
|
||||
assert cassette._path == os.path.join(os.path.dirname(__file__), 'function_name')
|
||||
function_name()
|
||||
|
||||
|
||||
def test_use_as_decorator_on_coroutine():
|
||||
original_http_connetion = httplib.HTTPConnection
|
||||
@Cassette.use(inject=True)
|
||||
def test_function(cassette):
|
||||
assert httplib.HTTPConnection.cassette is cassette
|
||||
assert httplib.HTTPConnection is not original_http_connetion
|
||||
value = yield 1
|
||||
assert value == 1
|
||||
assert httplib.HTTPConnection.cassette is cassette
|
||||
assert httplib.HTTPConnection is not original_http_connetion
|
||||
value = yield 2
|
||||
assert value == 2
|
||||
coroutine = test_function()
|
||||
value = next(coroutine)
|
||||
while True:
|
||||
try:
|
||||
value = coroutine.send(value)
|
||||
except StopIteration:
|
||||
break
|
||||
|
||||
|
||||
def test_use_as_decorator_on_generator():
|
||||
original_http_connetion = httplib.HTTPConnection
|
||||
@Cassette.use(inject=True)
|
||||
def test_function(cassette):
|
||||
assert httplib.HTTPConnection.cassette is cassette
|
||||
assert httplib.HTTPConnection is not original_http_connetion
|
||||
yield 1
|
||||
assert httplib.HTTPConnection.cassette is cassette
|
||||
assert httplib.HTTPConnection is not original_http_connetion
|
||||
yield 2
|
||||
assert list(test_function()) == [1, 2]
|
||||
|
||||
|
||||
def test_similar_requests(tmpdir):
|
||||
# WIP needs to be finished
|
||||
@Cassette.use(inject=True, match_on=(path, query, host, method))
|
||||
def test_function(cassette):
|
||||
conn = httplib.HTTPConnection("www.python.org")
|
||||
conn.request("GET", "/index.html?test=1")
|
||||
|
||||
conn = httplib.HTTPConnection("www.python.org")
|
||||
conn.request("GET", "/index.html?test=0")
|
||||
|
||||
conn = httplib.HTTPConnection("www.cool.org")
|
||||
conn.request("GET", "/index.html?test=0")
|
||||
cassette.similar_requests()
|
||||
|
||||
@@ -4,6 +4,7 @@ from vcr.filters import (
|
||||
remove_post_data_parameters
|
||||
)
|
||||
from vcr.request import Request
|
||||
import json
|
||||
|
||||
|
||||
def test_remove_headers():
|
||||
@@ -67,3 +68,29 @@ def test_remove_nonexistent_post_data_parameters():
|
||||
request = Request('POST', 'http://google.com', body, {})
|
||||
remove_post_data_parameters(request, ['id'])
|
||||
assert request.body == b''
|
||||
|
||||
|
||||
def test_remove_json_post_data_parameters():
|
||||
body = b'{"id": "secret", "foo": "bar", "baz": "qux"}'
|
||||
request = Request('POST', 'http://google.com', body, {})
|
||||
request.headers['Content-Type'] = 'application/json'
|
||||
remove_post_data_parameters(request, ['id'])
|
||||
request_body_json = json.loads(request.body.decode('utf-8'))
|
||||
expected_json = json.loads(b'{"foo": "bar", "baz": "qux"}'.decode('utf-8'))
|
||||
assert request_body_json == expected_json
|
||||
|
||||
|
||||
def test_remove_all_json_post_data_parameters():
|
||||
body = b'{"id": "secret", "foo": "bar"}'
|
||||
request = Request('POST', 'http://google.com', body, {})
|
||||
request.headers['Content-Type'] = 'application/json'
|
||||
remove_post_data_parameters(request, ['id', 'foo'])
|
||||
assert request.body == b'{}'
|
||||
|
||||
|
||||
def test_remove_nonexistent_json_post_data_parameters():
|
||||
body = b'{}'
|
||||
request = Request('POST', 'http://google.com', body, {})
|
||||
request.headers['Content-Type'] = 'application/json'
|
||||
remove_post_data_parameters(request, ['id'])
|
||||
assert request.body == b'{}'
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import itertools
|
||||
|
||||
import pytest
|
||||
|
||||
from vcr import matchers
|
||||
from vcr import request
|
||||
|
||||
@@ -35,6 +37,107 @@ def test_uri_matcher():
|
||||
assert matched
|
||||
|
||||
|
||||
req1_body = (b"<?xml version='1.0'?><methodCall><methodName>test</methodName>"
|
||||
b"<params><param><value><array><data><value><struct>"
|
||||
b"<member><name>a</name><value><string>1</string></value></member>"
|
||||
b"<member><name>b</name><value><string>2</string></value></member>"
|
||||
b"</struct></value></data></array></value></param></params></methodCall>")
|
||||
req2_body = (b"<?xml version='1.0'?><methodCall><methodName>test</methodName>"
|
||||
b"<params><param><value><array><data><value><struct>"
|
||||
b"<member><name>b</name><value><string>2</string></value></member>"
|
||||
b"<member><name>a</name><value><string>1</string></value></member>"
|
||||
b"</struct></value></data></array></value></param></params></methodCall>")
|
||||
|
||||
|
||||
@pytest.mark.parametrize("r1, r2", [
|
||||
(
|
||||
request.Request('POST', 'http://host.com/', '123', {}),
|
||||
request.Request('POST', 'http://another-host.com/',
|
||||
'123', {'Some-Header': 'value'})
|
||||
),
|
||||
(
|
||||
request.Request('POST', 'http://host.com/', 'a=1&b=2',
|
||||
{'Content-Type': 'application/x-www-form-urlencoded'}),
|
||||
request.Request('POST', 'http://host.com/', 'b=2&a=1',
|
||||
{'Content-Type': 'application/x-www-form-urlencoded'})
|
||||
),
|
||||
(
|
||||
request.Request('POST', 'http://host.com/', '123', {}),
|
||||
request.Request('POST', 'http://another-host.com/', '123', {'Some-Header': 'value'})
|
||||
),
|
||||
(
|
||||
request.Request(
|
||||
'POST', 'http://host.com/', 'a=1&b=2',
|
||||
{'Content-Type': 'application/x-www-form-urlencoded'}
|
||||
),
|
||||
request.Request(
|
||||
'POST', 'http://host.com/', 'b=2&a=1',
|
||||
{'Content-Type': 'application/x-www-form-urlencoded'}
|
||||
)
|
||||
),
|
||||
(
|
||||
request.Request(
|
||||
'POST', 'http://host.com/', '{"a": 1, "b": 2}',
|
||||
{'Content-Type': 'application/json'}
|
||||
),
|
||||
request.Request(
|
||||
'POST', 'http://host.com/', '{"b": 2, "a": 1}',
|
||||
{'content-type': 'application/json'}
|
||||
)
|
||||
),
|
||||
(
|
||||
request.Request(
|
||||
'POST', 'http://host.com/', req1_body,
|
||||
{'User-Agent': 'xmlrpclib', 'Content-Type': 'text/xml'}
|
||||
),
|
||||
request.Request(
|
||||
'POST', 'http://host.com/', req2_body,
|
||||
{'user-agent': 'somexmlrpc', 'content-type': 'text/xml'}
|
||||
)
|
||||
),
|
||||
(
|
||||
request.Request(
|
||||
'POST', 'http://host.com/',
|
||||
'{"a": 1, "b": 2}', {'Content-Type': 'application/json'}
|
||||
),
|
||||
request.Request(
|
||||
'POST', 'http://host.com/',
|
||||
'{"b": 2, "a": 1}', {'content-type': 'application/json'}
|
||||
)
|
||||
)
|
||||
])
|
||||
def test_body_matcher_does_match(r1, r2):
|
||||
assert matchers.body(r1, r2)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("r1, r2", [
|
||||
(
|
||||
request.Request('POST', 'http://host.com/', '{"a": 1, "b": 2}', {}),
|
||||
request.Request('POST', 'http://host.com/', '{"b": 2, "a": 1}', {}),
|
||||
),
|
||||
(
|
||||
request.Request(
|
||||
'POST', 'http://host.com/',
|
||||
'{"a": 1, "b": 3}', {'Content-Type': 'application/json'}
|
||||
),
|
||||
request.Request(
|
||||
'POST', 'http://host.com/',
|
||||
'{"b": 2, "a": 1}', {'content-type': 'application/json'}
|
||||
)
|
||||
),
|
||||
(
|
||||
request.Request(
|
||||
'POST', 'http://host.com/', req1_body, {'Content-Type': 'text/xml'}
|
||||
),
|
||||
request.Request(
|
||||
'POST', 'http://host.com/', req2_body, {'content-type': 'text/xml'}
|
||||
)
|
||||
)
|
||||
])
|
||||
def test_body_match_does_not_match(r1, r2):
|
||||
assert not matchers.body(r1, r2)
|
||||
|
||||
|
||||
def test_query_matcher():
|
||||
req1 = request.Request('GET', 'http://host.com/?a=b&c=d', '', {})
|
||||
req2 = request.Request('GET', 'http://host.com/?c=d&a=b', '', {})
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import pytest
|
||||
|
||||
from vcr.request import Request
|
||||
from vcr.request import Request, HeadersDict
|
||||
|
||||
|
||||
def test_str():
|
||||
@@ -12,11 +12,16 @@ def test_headers():
|
||||
headers = {'X-Header1': ['h1'], 'X-Header2': 'h2'}
|
||||
req = Request('GET', 'http://go.com/', '', headers)
|
||||
assert req.headers == {'X-Header1': 'h1', 'X-Header2': 'h2'}
|
||||
|
||||
req.add_header('X-Header1', 'h11')
|
||||
req.headers['X-Header1'] = 'h11'
|
||||
assert req.headers == {'X-Header1': 'h11', 'X-Header2': 'h2'}
|
||||
|
||||
|
||||
def test_add_header_deprecated():
|
||||
req = Request('GET', 'http://go.com/', '', {})
|
||||
pytest.deprecated_call(req.add_header, 'foo', 'bar')
|
||||
assert req.headers == {'foo': 'bar'}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("uri, expected_port", [
|
||||
('http://go.com/', 80),
|
||||
('http://go.com:80/', 80),
|
||||
@@ -36,3 +41,30 @@ def test_uri():
|
||||
|
||||
req = Request('GET', 'http://go.com:80/', '', {})
|
||||
assert req.uri == 'http://go.com:80/'
|
||||
|
||||
|
||||
def test_HeadersDict():
|
||||
|
||||
# Simple test of CaseInsensitiveDict
|
||||
h = HeadersDict()
|
||||
assert h == {}
|
||||
h['Content-Type'] = 'application/json'
|
||||
assert h == {'Content-Type': 'application/json'}
|
||||
assert h['content-type'] == 'application/json'
|
||||
assert h['CONTENT-TYPE'] == 'application/json'
|
||||
|
||||
# Test feature of HeadersDict: devolve list to first element
|
||||
h = HeadersDict()
|
||||
assert h == {}
|
||||
h['x'] = ['foo', 'bar']
|
||||
assert h == {'x': 'foo'}
|
||||
|
||||
# Test feature of HeadersDict: preserve original key case
|
||||
h = HeadersDict()
|
||||
assert h == {}
|
||||
h['Content-Type'] = 'application/json'
|
||||
assert h == {'Content-Type': 'application/json'}
|
||||
h['content-type'] = 'text/plain'
|
||||
assert h == {'Content-Type': 'text/plain'}
|
||||
h['CONtent-tyPE'] = 'whoa'
|
||||
assert h == {'Content-Type': 'whoa'}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import mock
|
||||
# -*- encoding: utf-8 -*-
|
||||
import pytest
|
||||
|
||||
from vcr.compat import mock
|
||||
from vcr.serialize import deserialize
|
||||
from vcr.serializers import yamlserializer, jsonserializer
|
||||
|
||||
@@ -27,6 +28,55 @@ def test_deserialize_new_json_cassette():
|
||||
deserialize(f.read(), jsonserializer)
|
||||
|
||||
|
||||
REQBODY_TEMPLATE = u'''\
|
||||
interactions:
|
||||
- request:
|
||||
body: {req_body}
|
||||
headers:
|
||||
Content-Type: [application/x-www-form-urlencoded]
|
||||
Host: [httpbin.org]
|
||||
method: POST
|
||||
uri: http://httpbin.org/post
|
||||
response:
|
||||
body: {{string: ""}}
|
||||
headers:
|
||||
content-length: ['0']
|
||||
content-type: [application/json]
|
||||
status: {{code: 200, message: OK}}
|
||||
'''
|
||||
|
||||
|
||||
# A cassette generated under Python 2 stores the request body as a string,
|
||||
# but the same cassette generated under Python 3 stores it as "!!binary".
|
||||
# Make sure we accept both forms, regardless of whether we're running under
|
||||
# Python 2 or 3.
|
||||
@pytest.mark.parametrize("req_body, expect", [
|
||||
# Cassette written under Python 2 (pure ASCII body)
|
||||
('x=5&y=2', b'x=5&y=2'),
|
||||
# Cassette written under Python 3 (pure ASCII body)
|
||||
('!!binary |\n eD01Jnk9Mg==', b'x=5&y=2'),
|
||||
|
||||
# Request body has non-ASCII chars (x=föo&y=2), encoded in UTF-8.
|
||||
('!!python/str "x=f\\xF6o&y=2"', b'x=f\xc3\xb6o&y=2'),
|
||||
('!!binary |\n eD1mw7ZvJnk9Mg==', b'x=f\xc3\xb6o&y=2'),
|
||||
|
||||
# Same request body, this time encoded in UTF-16. In this case, we
|
||||
# write the same YAML file under both Python 2 and 3, so there's only
|
||||
# one test case here.
|
||||
('!!binary |\n //54AD0AZgD2AG8AJgB5AD0AMgA=',
|
||||
b'\xff\xfex\x00=\x00f\x00\xf6\x00o\x00&\x00y\x00=\x002\x00'),
|
||||
|
||||
# Same again, this time encoded in ISO-8859-1.
|
||||
('!!binary |\n eD1m9m8meT0y', b'x=f\xf6o&y=2'),
|
||||
])
|
||||
def test_deserialize_py2py3_yaml_cassette(tmpdir, req_body, expect):
|
||||
cfile = tmpdir.join('test_cassette.yaml')
|
||||
cfile.write(REQBODY_TEMPLATE.format(req_body=req_body))
|
||||
with open(str(cfile)) as f:
|
||||
(requests, responses) = deserialize(f.read(), yamlserializer)
|
||||
assert requests[0].body == expect
|
||||
|
||||
|
||||
@mock.patch.object(jsonserializer.json, 'dumps',
|
||||
side_effect=UnicodeDecodeError('utf-8', b'unicode error in serialization',
|
||||
0, 10, 'blew up'))
|
||||
|
||||
7
tests/unit/test_stubs.py
Normal file
7
tests/unit/test_stubs.py
Normal file
@@ -0,0 +1,7 @@
|
||||
from vcr.stubs import VCRHTTPSConnection
|
||||
|
||||
class TestVCRConnection(object):
|
||||
def test_setting_of_attributes_get_propogated_to_real_connection(self):
|
||||
vcr_connection = VCRHTTPSConnection('www.examplehost.com')
|
||||
vcr_connection.ssl_version = 'example_ssl_version'
|
||||
assert vcr_connection.real_connection.ssl_version == 'example_ssl_version'
|
||||
@@ -1,18 +1,27 @@
|
||||
import mock
|
||||
import os
|
||||
|
||||
import pytest
|
||||
from six.moves import http_client as httplib
|
||||
|
||||
from vcr import VCR, use_cassette
|
||||
from vcr.compat import mock
|
||||
from vcr.request import Request
|
||||
from vcr.stubs import VCRHTTPSConnection
|
||||
from vcr.patch import _HTTPConnection, force_reset
|
||||
|
||||
|
||||
def test_vcr_use_cassette():
|
||||
record_mode = mock.Mock()
|
||||
test_vcr = VCR(record_mode=record_mode)
|
||||
with mock.patch('vcr.cassette.Cassette.load', return_value=mock.MagicMock(inject=False)) as mock_cassette_load:
|
||||
with mock.patch(
|
||||
'vcr.cassette.Cassette.load',
|
||||
return_value=mock.MagicMock(inject=False)
|
||||
) as mock_cassette_load:
|
||||
|
||||
@test_vcr.use_cassette('test')
|
||||
def function():
|
||||
pass
|
||||
|
||||
assert mock_cassette_load.call_count == 0
|
||||
function()
|
||||
assert mock_cassette_load.call_args[1]['record_mode'] is record_mode
|
||||
@@ -33,9 +42,11 @@ def test_vcr_use_cassette():
|
||||
|
||||
def test_vcr_before_record_request_params():
|
||||
base_path = 'http://httpbin.org/'
|
||||
|
||||
def before_record_cb(request):
|
||||
if request.path != '/get':
|
||||
return request
|
||||
|
||||
test_vcr = VCR(filter_headers=('cookie',), before_record_request=before_record_cb,
|
||||
ignore_hosts=('www.test.com',), ignore_localhost=True,
|
||||
filter_query_parameters=('foo',))
|
||||
@@ -48,8 +59,12 @@ def test_vcr_before_record_request_params():
|
||||
assert cassette.filter_request(
|
||||
Request('GET', base_path + '?foo=bar', '',
|
||||
{'cookie': 'test', 'other': 'fun'})).headers == {'other': 'fun'}
|
||||
assert cassette.filter_request(Request('GET', base_path + '?foo=bar', '',
|
||||
{'cookie': 'test', 'other': 'fun'})).headers == {'other': 'fun'}
|
||||
assert cassette.filter_request(
|
||||
Request(
|
||||
'GET', base_path + '?foo=bar', '',
|
||||
{'cookie': 'test', 'other': 'fun'}
|
||||
)
|
||||
).headers == {'other': 'fun'}
|
||||
|
||||
assert cassette.filter_request(Request('GET', 'http://www.test.com' + '?foo=bar', '',
|
||||
{'cookie': 'test', 'other': 'fun'})) is None
|
||||
@@ -59,6 +74,69 @@ def test_vcr_before_record_request_params():
|
||||
assert cassette.filter_request(Request('GET', base_path + 'get', '', {})) is not None
|
||||
|
||||
|
||||
def test_vcr_before_record_response_iterable():
|
||||
# Regression test for #191
|
||||
|
||||
request = Request('GET', '/', '', {})
|
||||
response = object() # just can't be None
|
||||
|
||||
# Prevent actually saving the cassette
|
||||
with mock.patch('vcr.cassette.save_cassette'):
|
||||
|
||||
# Baseline: non-iterable before_record_response should work
|
||||
mock_filter = mock.Mock()
|
||||
vcr = VCR(before_record_response=mock_filter)
|
||||
with vcr.use_cassette('test') as cassette:
|
||||
assert mock_filter.call_count == 0
|
||||
cassette.append(request, response)
|
||||
assert mock_filter.call_count == 1
|
||||
|
||||
# Regression test: iterable before_record_response should work too
|
||||
mock_filter = mock.Mock()
|
||||
vcr = VCR(before_record_response=(mock_filter,))
|
||||
with vcr.use_cassette('test') as cassette:
|
||||
assert mock_filter.call_count == 0
|
||||
cassette.append(request, response)
|
||||
assert mock_filter.call_count == 1
|
||||
|
||||
|
||||
def test_before_record_response_as_filter():
|
||||
request = Request('GET', '/', '', {})
|
||||
response = object() # just can't be None
|
||||
|
||||
# Prevent actually saving the cassette
|
||||
with mock.patch('vcr.cassette.save_cassette'):
|
||||
|
||||
filter_all = mock.Mock(return_value=None)
|
||||
vcr = VCR(before_record_response=filter_all)
|
||||
with vcr.use_cassette('test') as cassette:
|
||||
cassette.append(request, response)
|
||||
assert cassette.data == []e
|
||||
assert not cassette.dirty
|
||||
|
||||
|
||||
def test_vcr_path_transformer():
|
||||
# Regression test for #199
|
||||
|
||||
# Prevent actually saving the cassette
|
||||
with mock.patch('vcr.cassette.save_cassette'):
|
||||
|
||||
# Baseline: path should be unchanged
|
||||
vcr = VCR()
|
||||
with vcr.use_cassette('test') as cassette:
|
||||
assert cassette._path == 'test'
|
||||
|
||||
# Regression test: path_transformer=None should do the same.
|
||||
vcr = VCR(path_transformer=None)
|
||||
with vcr.use_cassette('test') as cassette:
|
||||
assert cassette._path == 'test'
|
||||
|
||||
# and it should still work with cassette_library_dir
|
||||
vcr = VCR(cassette_library_dir='/foo')
|
||||
with vcr.use_cassette('test') as cassette:
|
||||
assert cassette._path == '/foo/test'
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def random_fixture():
|
||||
return 1
|
||||
@@ -87,7 +165,10 @@ def test_custom_patchers():
|
||||
assert issubclass(Test.attribute, VCRHTTPSConnection)
|
||||
assert VCRHTTPSConnection is not Test.attribute
|
||||
|
||||
with test_vcr.use_cassette('custom_patches', custom_patches=((Test, 'attribute2', VCRHTTPSConnection),)):
|
||||
with test_vcr.use_cassette(
|
||||
'custom_patches',
|
||||
custom_patches=((Test, 'attribute2', VCRHTTPSConnection),)
|
||||
):
|
||||
assert issubclass(Test.attribute, VCRHTTPSConnection)
|
||||
assert VCRHTTPSConnection is not Test.attribute
|
||||
assert Test.attribute is Test.attribute2
|
||||
@@ -95,6 +176,7 @@ def test_custom_patchers():
|
||||
|
||||
def test_inject_cassette():
|
||||
vcr = VCR(inject_cassette=True)
|
||||
|
||||
@vcr.use_cassette('test', record_mode='once')
|
||||
def with_cassette_injected(cassette):
|
||||
assert cassette.record_mode == 'once'
|
||||
@@ -109,9 +191,11 @@ def test_inject_cassette():
|
||||
|
||||
def test_with_current_defaults():
|
||||
vcr = VCR(inject_cassette=True, record_mode='once')
|
||||
|
||||
@vcr.use_cassette('test', with_current_defaults=False)
|
||||
def changing_defaults(cassette, checks):
|
||||
checks(cassette)
|
||||
|
||||
@vcr.use_cassette('test', with_current_defaults=True)
|
||||
def current_defaults(cassette, checks):
|
||||
checks(cassette)
|
||||
@@ -128,3 +212,142 @@ def test_with_current_defaults():
|
||||
vcr.record_mode = 'all'
|
||||
changing_defaults(assert_record_mode_all)
|
||||
current_defaults(assert_record_mode_once)
|
||||
|
||||
|
||||
def test_cassette_library_dir_with_decoration_and_no_explicit_path():
|
||||
library_dir = '/libary_dir'
|
||||
vcr = VCR(inject_cassette=True, cassette_library_dir=library_dir)
|
||||
|
||||
@vcr.use_cassette()
|
||||
def function_name(cassette):
|
||||
assert cassette._path == os.path.join(library_dir, 'function_name')
|
||||
|
||||
function_name()
|
||||
|
||||
|
||||
def test_cassette_library_dir_with_decoration_and_explicit_path():
|
||||
library_dir = '/libary_dir'
|
||||
vcr = VCR(inject_cassette=True, cassette_library_dir=library_dir)
|
||||
|
||||
@vcr.use_cassette(path='custom_name')
|
||||
def function_name(cassette):
|
||||
assert cassette._path == os.path.join(library_dir, 'custom_name')
|
||||
|
||||
function_name()
|
||||
|
||||
|
||||
def test_cassette_library_dir_with_decoration_and_super_explicit_path():
|
||||
library_dir = '/libary_dir'
|
||||
vcr = VCR(inject_cassette=True, cassette_library_dir=library_dir)
|
||||
|
||||
@vcr.use_cassette(path=os.path.join(library_dir, 'custom_name'))
|
||||
def function_name(cassette):
|
||||
assert cassette._path == os.path.join(library_dir, 'custom_name')
|
||||
|
||||
function_name()
|
||||
|
||||
|
||||
def test_cassette_library_dir_with_path_transformer():
|
||||
library_dir = '/libary_dir'
|
||||
vcr = VCR(inject_cassette=True, cassette_library_dir=library_dir,
|
||||
path_transformer=lambda path: path + '.json')
|
||||
|
||||
@vcr.use_cassette()
|
||||
def function_name(cassette):
|
||||
assert cassette._path == os.path.join(library_dir, 'function_name.json')
|
||||
|
||||
function_name()
|
||||
|
||||
|
||||
def test_use_cassette_with_no_extra_invocation():
|
||||
vcr = VCR(inject_cassette=True, cassette_library_dir='/')
|
||||
|
||||
@vcr.use_cassette
|
||||
def function_name(cassette):
|
||||
assert cassette._path == os.path.join('/', 'function_name')
|
||||
|
||||
function_name()
|
||||
|
||||
|
||||
def test_path_transformer():
|
||||
vcr = VCR(inject_cassette=True, cassette_library_dir='/',
|
||||
path_transformer=lambda x: x + '_test')
|
||||
|
||||
@vcr.use_cassette
|
||||
def function_name(cassette):
|
||||
assert cassette._path == os.path.join('/', 'function_name_test')
|
||||
|
||||
function_name()
|
||||
|
||||
|
||||
def test_cassette_name_generator_defaults_to_using_module_function_defined_in():
|
||||
vcr = VCR(inject_cassette=True)
|
||||
|
||||
@vcr.use_cassette
|
||||
def function_name(cassette):
|
||||
assert cassette._path == os.path.join(os.path.dirname(__file__),
|
||||
'function_name')
|
||||
function_name()
|
||||
|
||||
|
||||
def test_ensure_suffix():
|
||||
vcr = VCR(inject_cassette=True, path_transformer=VCR.ensure_suffix('.yaml'))
|
||||
|
||||
@vcr.use_cassette
|
||||
def function_name(cassette):
|
||||
assert cassette._path == os.path.join(os.path.dirname(__file__),
|
||||
'function_name.yaml')
|
||||
|
||||
function_name()
|
||||
|
||||
|
||||
def test_additional_matchers():
|
||||
vcr = VCR(match_on=('uri',), inject_cassette=True)
|
||||
|
||||
@vcr.use_cassette
|
||||
def function_defaults(cassette):
|
||||
assert set(cassette._match_on) == set([vcr.matchers['uri']])
|
||||
|
||||
@vcr.use_cassette(additional_matchers=('body',))
|
||||
def function_additional(cassette):
|
||||
assert set(cassette._match_on) == set([vcr.matchers['uri'], vcr.matchers['body']])
|
||||
|
||||
function_defaults()
|
||||
function_additional()
|
||||
|
||||
|
||||
def test_decoration_should_respect_function_return_value():
|
||||
vcr = VCR()
|
||||
ret = 'a-return-value'
|
||||
|
||||
@vcr.use_cassette
|
||||
def function_with_return():
|
||||
return ret
|
||||
|
||||
assert ret == function_with_return()
|
||||
|
||||
|
||||
class TestVCRClass(VCR().test_case()):
|
||||
|
||||
def no_decoration(self):
|
||||
assert httplib.HTTPConnection == _HTTPConnection
|
||||
self.test_dynamically_added()
|
||||
assert httplib.HTTPConnection == _HTTPConnection
|
||||
|
||||
def test_one(self):
|
||||
with force_reset():
|
||||
self.no_decoration()
|
||||
with force_reset():
|
||||
self.test_two()
|
||||
assert httplib.HTTPConnection != _HTTPConnection
|
||||
|
||||
def test_two(self):
|
||||
assert httplib.HTTPConnection != _HTTPConnection
|
||||
|
||||
|
||||
def test_dynamically_added(self):
|
||||
assert httplib.HTTPConnection != _HTTPConnection
|
||||
|
||||
|
||||
TestVCRClass.test_dynamically_added = test_dynamically_added
|
||||
del test_dynamically_added
|
||||
|
||||
7
tox.ini
7
tox.ini
@@ -1,5 +1,5 @@
|
||||
[tox]
|
||||
envlist = {py26,py27,py33,py34,pypy}-{requests25,requests24,requests23,requests22,requests1,httplib2,urllib317,urllib319,urllib3110,boto}
|
||||
envlist = {py26,py27,py33,py34,pypy}-{requests27,requests26,requests25,requests24,requests23,requests22,requests1,httplib2,urllib317,urllib319,urllib3110,tornado,boto}
|
||||
|
||||
[testenv]
|
||||
commands =
|
||||
@@ -16,6 +16,8 @@ deps =
|
||||
pytest-localserver
|
||||
PyYAML
|
||||
requests1: requests==1.2.3
|
||||
requests27: requests==2.7.0
|
||||
requests26: requests==2.6.0
|
||||
requests25: requests==2.5.0
|
||||
requests24: requests==2.4.0
|
||||
requests23: requests==2.3.0
|
||||
@@ -24,4 +26,7 @@ deps =
|
||||
urllib317: urllib3==1.7.1
|
||||
urllib319: urllib3==1.9.1
|
||||
urllib3110: urllib3==1.10.2
|
||||
{py26,py27,py33,py34,pypy}-tornado: tornado
|
||||
{py26,py27,py33,py34,pypy}-tornado: pytest-tornado
|
||||
{py26,py27,py33,py34}-tornado: pycurl
|
||||
boto: boto
|
||||
|
||||
188
vcr/cassette.py
188
vcr/cassette.py
@@ -1,19 +1,17 @@
|
||||
"""The container for recorded requests and responses"""
|
||||
import inspect
|
||||
import logging
|
||||
import operator
|
||||
import sys
|
||||
|
||||
import contextlib2
|
||||
import wrapt
|
||||
try:
|
||||
from collections import Counter
|
||||
except ImportError:
|
||||
from backport_collections import Counter
|
||||
|
||||
# Internal imports
|
||||
from .compat import contextlib, collections
|
||||
from .errors import UnhandledHTTPRequestError
|
||||
from .matchers import requests_match, uri, method
|
||||
from .patch import CassettePatcherBuilder
|
||||
from .persist import load_cassette, save_cassette
|
||||
from .serializers import yamlserializer
|
||||
from .matchers import requests_match, uri, method
|
||||
from .errors import UnhandledHTTPRequestError
|
||||
from .util import partition_dict
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
@@ -23,15 +21,25 @@ class CassetteContextDecorator(object):
|
||||
"""Context manager/decorator that handles installing the cassette and
|
||||
removing cassettes.
|
||||
|
||||
This class defers the creation of a new cassette instance until the point at
|
||||
which it is installed by context manager or decorator. The fact that a new
|
||||
cassette is used with each application prevents the state of any cassette
|
||||
from interfering with another.
|
||||
This class defers the creation of a new cassette instance until
|
||||
the point at which it is installed by context manager or
|
||||
decorator. The fact that a new cassette is used with each
|
||||
application prevents the state of any cassette from interfering
|
||||
with another.
|
||||
|
||||
Instances of this class are NOT reentrant as context managers.
|
||||
However, functions that are decorated by
|
||||
``CassetteContextDecorator`` instances ARE reentrant. See the
|
||||
implementation of ``__call__`` on this class for more details.
|
||||
There is also a guard against attempts to reenter instances of
|
||||
this class as a context manager in ``__exit__``.
|
||||
"""
|
||||
|
||||
_non_cassette_arguments = ('path_transformer', 'func_path_generator')
|
||||
|
||||
@classmethod
|
||||
def from_args(cls, cassette_class, path, **kwargs):
|
||||
return cls(cassette_class, lambda: (path, kwargs))
|
||||
def from_args(cls, cassette_class, **kwargs):
|
||||
return cls(cassette_class, lambda: dict(kwargs))
|
||||
|
||||
def __init__(self, cls, args_getter):
|
||||
self.cls = cls
|
||||
@@ -39,20 +47,39 @@ class CassetteContextDecorator(object):
|
||||
self.__finish = None
|
||||
|
||||
def _patch_generator(self, cassette):
|
||||
with contextlib2.ExitStack() as exit_stack:
|
||||
with contextlib.ExitStack() as exit_stack:
|
||||
for patcher in CassettePatcherBuilder(cassette).build():
|
||||
exit_stack.enter_context(patcher)
|
||||
log.debug('Entered context for cassette at {0}.'.format(cassette._path))
|
||||
log_format = '{action} context for cassette at {path}.'
|
||||
log.debug(log_format.format(
|
||||
action="Entering", path=cassette._path
|
||||
))
|
||||
yield cassette
|
||||
log.debug('Exiting context for cassette at {0}.'.format(cassette._path))
|
||||
log.debug(log_format.format(
|
||||
action="Exiting", path=cassette._path
|
||||
))
|
||||
# TODO(@IvanMalison): Hmmm. it kind of feels like this should be
|
||||
# somewhere else.
|
||||
cassette._save()
|
||||
|
||||
def __enter__(self):
|
||||
# This assertion is here to prevent the dangerous behavior
|
||||
# that would result from forgetting about a __finish before
|
||||
# completing it.
|
||||
# How might this condition be met? Here is an example:
|
||||
# context_decorator = Cassette.use('whatever')
|
||||
# with context_decorator:
|
||||
# with context_decorator:
|
||||
# pass
|
||||
assert self.__finish is None, "Cassette already open."
|
||||
path, kwargs = self._args_getter()
|
||||
self.__finish = self._patch_generator(self.cls.load(path, **kwargs))
|
||||
other_kwargs, cassette_kwargs = partition_dict(
|
||||
lambda key, _: key in self._non_cassette_arguments,
|
||||
self._args_getter()
|
||||
)
|
||||
if other_kwargs.get('path_transformer'):
|
||||
transformer = other_kwargs['path_transformer']
|
||||
cassette_kwargs['path'] = transformer(cassette_kwargs['path'])
|
||||
self.__finish = self._patch_generator(self.cls.load(**cassette_kwargs))
|
||||
return next(self.__finish)
|
||||
|
||||
def __exit__(self, *args):
|
||||
@@ -61,20 +88,95 @@ class CassetteContextDecorator(object):
|
||||
|
||||
@wrapt.decorator
|
||||
def __call__(self, function, instance, args, kwargs):
|
||||
# This awkward cloning thing is done to ensure that decorated
|
||||
# functions are reentrant. This is required for thread
|
||||
# safety and the correct operation of recursive functions.
|
||||
args_getter = self._build_args_getter_for_decorator(function)
|
||||
return type(self)(self.cls, args_getter)._execute_function(
|
||||
function, args, kwargs
|
||||
)
|
||||
|
||||
def _execute_function(self, function, args, kwargs):
|
||||
if inspect.isgeneratorfunction(function):
|
||||
handler = self._handle_coroutine
|
||||
else:
|
||||
handler = self._handle_function
|
||||
return handler(function, args, kwargs)
|
||||
|
||||
def _handle_coroutine(self, function, args, kwargs):
|
||||
"""Wraps a coroutine so that we're inside the cassette context for the
|
||||
duration of the coroutine.
|
||||
"""
|
||||
with self as cassette:
|
||||
if cassette.inject:
|
||||
return function(cassette, *args, **kwargs)
|
||||
else:
|
||||
return function(*args, **kwargs)
|
||||
coroutine = self.__handle_function(cassette, function, args, kwargs)
|
||||
# We don't need to catch StopIteration. The caller (Tornado's
|
||||
# gen.coroutine, for example) will handle that.
|
||||
to_yield = next(coroutine)
|
||||
while True:
|
||||
try:
|
||||
to_send = yield to_yield
|
||||
except Exception:
|
||||
to_yield = coroutine.throw(*sys.exc_info())
|
||||
else:
|
||||
to_yield = coroutine.send(to_send)
|
||||
|
||||
def __handle_function(self, cassette, function, args, kwargs):
|
||||
if cassette.inject:
|
||||
return function(cassette, *args, **kwargs)
|
||||
else:
|
||||
return function(*args, **kwargs)
|
||||
|
||||
def _handle_function(self, function, args, kwargs):
|
||||
with self as cassette:
|
||||
return self.__handle_function(cassette, function, args, kwargs)
|
||||
|
||||
@staticmethod
|
||||
def get_function_name(function):
|
||||
return function.__name__
|
||||
|
||||
def _build_args_getter_for_decorator(self, function):
|
||||
def new_args_getter():
|
||||
kwargs = self._args_getter()
|
||||
if 'path' not in kwargs:
|
||||
name_generator = (kwargs.get('func_path_generator') or
|
||||
self.get_function_name)
|
||||
path = name_generator(function)
|
||||
kwargs['path'] = path
|
||||
return kwargs
|
||||
return new_args_getter
|
||||
|
||||
|
||||
class SimilarityScorer(object):
|
||||
|
||||
def __init__(self, matchers, request, ascending=False):
|
||||
self._matchers = matchers
|
||||
self._request = request
|
||||
self._ascending = False
|
||||
|
||||
def score(self, candidate, play_count):
|
||||
value = 1
|
||||
total = 0
|
||||
if play_count < 1:
|
||||
total += value
|
||||
if self._ascending:
|
||||
value *= 2
|
||||
for matcher in self._matchers[::-1]:
|
||||
if matcher(self._request, candidate):
|
||||
total += value
|
||||
if self._ascending:
|
||||
value *= 2
|
||||
return total
|
||||
|
||||
|
||||
class Cassette(object):
|
||||
"""A container for recorded requests and responses"""
|
||||
|
||||
max_playcount = 1
|
||||
|
||||
@classmethod
|
||||
def load(cls, path, **kwargs):
|
||||
def load(cls, **kwargs):
|
||||
"""Instantiate and load the cassette stored at the specified path."""
|
||||
new_cassette = cls(path, **kwargs)
|
||||
new_cassette = cls(**kwargs)
|
||||
new_cassette._load()
|
||||
return new_cassette
|
||||
|
||||
@@ -83,26 +185,27 @@ class Cassette(object):
|
||||
return CassetteContextDecorator(cls, arg_getter)
|
||||
|
||||
@classmethod
|
||||
def use(cls, *args, **kwargs):
|
||||
return CassetteContextDecorator.from_args(cls, *args, **kwargs)
|
||||
def use(cls, **kwargs):
|
||||
return CassetteContextDecorator.from_args(cls, **kwargs)
|
||||
|
||||
def __init__(self, path, serializer=yamlserializer, record_mode='once',
|
||||
match_on=(uri, method), before_record_request=None,
|
||||
before_record_response=None, custom_patches=(),
|
||||
inject=False):
|
||||
match_on=(uri, method), before_record_request=None,
|
||||
before_record_response=None, custom_patches=(), inject=False,
|
||||
similarity_scorer_factory=None):
|
||||
|
||||
self._path = path
|
||||
self._serializer = serializer
|
||||
self._match_on = match_on
|
||||
self._before_record_request = before_record_request or (lambda x: x)
|
||||
self._before_record_response = before_record_response or (lambda x: x)
|
||||
self._similarity_scorer_factory = similarity_scorer_factory or SimilarityScorer
|
||||
self.inject = inject
|
||||
self.record_mode = record_mode
|
||||
self.custom_patches = custom_patches
|
||||
|
||||
# self.data is the list of (req, resp) tuples
|
||||
self.data = []
|
||||
self.play_counts = Counter()
|
||||
self.play_counts = collections.Counter()
|
||||
self.dirty = False
|
||||
self.rewound = False
|
||||
|
||||
@@ -133,8 +236,9 @@ class Cassette(object):
|
||||
request = self._before_record_request(request)
|
||||
if not request:
|
||||
return
|
||||
if self._before_record_response:
|
||||
response = self._before_record_response(response)
|
||||
response = self._before_record_response(response)
|
||||
if response is None:
|
||||
return
|
||||
self.data.append((request, response))
|
||||
self.dirty = True
|
||||
|
||||
@@ -151,6 +255,20 @@ class Cassette(object):
|
||||
if requests_match(request, stored_request, self._match_on):
|
||||
yield index, response
|
||||
|
||||
def failing_matchers(self, a, b):
|
||||
return [matcher for matcher in self._match_on if not matcher(a, b)]
|
||||
|
||||
def similar_requests(self, request):
|
||||
scorer = self._similarity_scorer_factory(self._match_on, request).score
|
||||
scored_requests = [
|
||||
(
|
||||
stored_request,
|
||||
scorer(stored_request, self.play_counts[index])
|
||||
)
|
||||
for index, (stored_request, response) in enumerate(self.data)
|
||||
]
|
||||
return sorted(scored_requests, key=operator.itemgetter(1), reverse=True)
|
||||
|
||||
def can_play_response_for(self, request):
|
||||
request = self._before_record_request(request)
|
||||
return request and request in self and \
|
||||
@@ -163,7 +281,7 @@ class Cassette(object):
|
||||
hasn't been played back before, and mark it as played
|
||||
"""
|
||||
for index, response in self._responses(request):
|
||||
if self.play_counts[index] == 0:
|
||||
if self.play_counts[index] < self.max_playcount:
|
||||
self.play_counts[index] += 1
|
||||
return response
|
||||
# The cassette doesn't contain the request asked for.
|
||||
|
||||
18
vcr/compat.py
Normal file
18
vcr/compat.py
Normal file
@@ -0,0 +1,18 @@
|
||||
try:
|
||||
from unittest import mock
|
||||
except ImportError:
|
||||
import mock
|
||||
|
||||
try:
|
||||
import contextlib
|
||||
except ImportError:
|
||||
import contextlib2 as contextlib
|
||||
else:
|
||||
if not hasattr(contextlib, 'ExitStack'):
|
||||
import contextlib2 as contextlib
|
||||
|
||||
import collections
|
||||
if not hasattr(collections, 'Counter'):
|
||||
import backport_collections as collections
|
||||
|
||||
__all__ = ['mock', 'contextlib', 'collections']
|
||||
156
vcr/config.py
156
vcr/config.py
@@ -1,23 +1,41 @@
|
||||
import collections
|
||||
import copy
|
||||
import functools
|
||||
import inspect
|
||||
import os
|
||||
import types
|
||||
|
||||
import six
|
||||
|
||||
from .compat import collections
|
||||
from .cassette import Cassette
|
||||
from .serializers import yamlserializer, jsonserializer
|
||||
from .util import compose, auto_decorate
|
||||
from . import matchers
|
||||
from . import filters
|
||||
|
||||
|
||||
class VCR(object):
|
||||
|
||||
def __init__(self, serializer='yaml', cassette_library_dir=None,
|
||||
record_mode="once", filter_headers=(), ignore_localhost=False,
|
||||
custom_patches=(), filter_query_parameters=(),
|
||||
filter_post_data_parameters=(), before_record_request=None,
|
||||
before_record_response=None, ignore_hosts=(),
|
||||
@staticmethod
|
||||
def is_test_method(method_name, function):
|
||||
return method_name.startswith('test') and \
|
||||
isinstance(function, types.FunctionType)
|
||||
|
||||
@staticmethod
|
||||
def ensure_suffix(suffix):
|
||||
def ensure(path):
|
||||
if not path.endswith(suffix):
|
||||
return path + suffix
|
||||
return path
|
||||
return ensure
|
||||
|
||||
def __init__(self, path_transformer=None, before_record_request=None,
|
||||
custom_patches=(), filter_query_parameters=(), ignore_hosts=(),
|
||||
record_mode="once", ignore_localhost=False, filter_headers=(),
|
||||
before_record_response=None, filter_post_data_parameters=(),
|
||||
match_on=('method', 'scheme', 'host', 'port', 'path', 'query'),
|
||||
before_record=None, inject_cassette=False):
|
||||
before_record=None, inject_cassette=False, serializer='yaml',
|
||||
cassette_library_dir=None, func_path_generator=None):
|
||||
self.serializer = serializer
|
||||
self.match_on = match_on
|
||||
self.cassette_library_dir = cassette_library_dir
|
||||
@@ -35,6 +53,7 @@ class VCR(object):
|
||||
'path': matchers.path,
|
||||
'query': matchers.query,
|
||||
'headers': matchers.headers,
|
||||
'raw_body': matchers.raw_body,
|
||||
'body': matchers.body,
|
||||
}
|
||||
self.record_mode = record_mode
|
||||
@@ -46,16 +65,19 @@ class VCR(object):
|
||||
self.ignore_hosts = ignore_hosts
|
||||
self.ignore_localhost = ignore_localhost
|
||||
self.inject_cassette = inject_cassette
|
||||
self.path_transformer = path_transformer
|
||||
self.func_path_generator = func_path_generator
|
||||
self._custom_patches = tuple(custom_patches)
|
||||
|
||||
def _get_serializer(self, serializer_name):
|
||||
try:
|
||||
serializer = self.serializers[serializer_name]
|
||||
except KeyError:
|
||||
print("Serializer {0} doesn't exist or isn't registered".format(
|
||||
serializer_name
|
||||
))
|
||||
raise KeyError
|
||||
raise KeyError(
|
||||
"Serializer {0} doesn't exist or isn't registered".format(
|
||||
serializer_name
|
||||
)
|
||||
)
|
||||
return serializer
|
||||
|
||||
def _get_matchers(self, matcher_names):
|
||||
@@ -69,53 +91,84 @@ class VCR(object):
|
||||
)
|
||||
return matchers
|
||||
|
||||
def use_cassette(self, path, with_current_defaults=False, **kwargs):
|
||||
def use_cassette(self, path=None, **kwargs):
|
||||
if path is not None and not isinstance(path, six.string_types):
|
||||
function = path
|
||||
# Assume this is an attempt to decorate a function
|
||||
return self._use_cassette(**kwargs)(function)
|
||||
return self._use_cassette(path=path, **kwargs)
|
||||
|
||||
def _use_cassette(self, with_current_defaults=False, **kwargs):
|
||||
if with_current_defaults:
|
||||
path, config = self.get_path_and_merged_config(path, **kwargs)
|
||||
return Cassette.use(path, **config)
|
||||
config = self.get_merged_config(**kwargs)
|
||||
return Cassette.use(**config)
|
||||
# This is made a function that evaluates every time a cassette
|
||||
# is made so that changes that are made to this VCR instance
|
||||
# that occur AFTER the `use_cassette` decorator is applied
|
||||
# still affect subsequent calls to the decorated function.
|
||||
args_getter = functools.partial(self.get_path_and_merged_config,
|
||||
path, **kwargs)
|
||||
args_getter = functools.partial(self.get_merged_config, **kwargs)
|
||||
return Cassette.use_arg_getter(args_getter)
|
||||
|
||||
def get_path_and_merged_config(self, path, **kwargs):
|
||||
def get_merged_config(self, **kwargs):
|
||||
serializer_name = kwargs.get('serializer', self.serializer)
|
||||
matcher_names = kwargs.get('match_on', self.match_on)
|
||||
path_transformer = kwargs.get(
|
||||
'path_transformer',
|
||||
self.path_transformer
|
||||
)
|
||||
func_path_generator = kwargs.get(
|
||||
'func_path_generator',
|
||||
self.func_path_generator
|
||||
)
|
||||
cassette_library_dir = kwargs.get(
|
||||
'cassette_library_dir',
|
||||
self.cassette_library_dir
|
||||
)
|
||||
additional_matchers = kwargs.get('additional_matchers', ())
|
||||
|
||||
if cassette_library_dir:
|
||||
path = os.path.join(cassette_library_dir, path)
|
||||
def add_cassette_library_dir(path):
|
||||
if not path.startswith(cassette_library_dir):
|
||||
return os.path.join(cassette_library_dir, path)
|
||||
return path
|
||||
path_transformer = compose(
|
||||
add_cassette_library_dir, path_transformer
|
||||
)
|
||||
elif not func_path_generator:
|
||||
# If we don't have a library dir, use the functions
|
||||
# location to build a full path for cassettes.
|
||||
func_path_generator = self._build_path_from_func_using_module
|
||||
|
||||
merged_config = {
|
||||
'serializer': self._get_serializer(serializer_name),
|
||||
'match_on': self._get_matchers(matcher_names),
|
||||
'match_on': self._get_matchers(
|
||||
tuple(matcher_names) + tuple(additional_matchers)
|
||||
),
|
||||
'record_mode': kwargs.get('record_mode', self.record_mode),
|
||||
'before_record_request': self._build_before_record_request(kwargs),
|
||||
'before_record_response': self._build_before_record_response(
|
||||
kwargs
|
||||
),
|
||||
'before_record_response': self._build_before_record_response(kwargs),
|
||||
'custom_patches': self._custom_patches + kwargs.get(
|
||||
'custom_patches', ()
|
||||
),
|
||||
'inject': kwargs.get('inject_cassette', self.inject_cassette)
|
||||
'inject': kwargs.get('inject_cassette', self.inject_cassette),
|
||||
'path_transformer': path_transformer,
|
||||
'func_path_generator': func_path_generator
|
||||
}
|
||||
return path, merged_config
|
||||
path = kwargs.get('path')
|
||||
if path:
|
||||
merged_config['path'] = path
|
||||
return merged_config
|
||||
|
||||
def _build_before_record_response(self, options):
|
||||
before_record_response = options.get(
|
||||
'before_record_response', self.before_record_response
|
||||
)
|
||||
filter_functions = []
|
||||
if before_record_response and not isinstance(before_record_response,
|
||||
collections.Iterable):
|
||||
before_record_response = (before_record_response,)
|
||||
for function in before_record_response:
|
||||
filter_functions.append(function)
|
||||
if before_record_response:
|
||||
if not isinstance(before_record_response, collections.Iterable):
|
||||
before_record_response = (before_record_response,)
|
||||
filter_functions.extend(before_record_response)
|
||||
|
||||
def before_record_response(response):
|
||||
for function in filter_functions:
|
||||
if response is None:
|
||||
@@ -136,7 +189,8 @@ class VCR(object):
|
||||
'filter_post_data_parameters', self.filter_post_data_parameters
|
||||
)
|
||||
before_record_request = options.get(
|
||||
"before_record_request", options.get("before_record", self.before_record_request)
|
||||
"before_record_request",
|
||||
options.get("before_record", self.before_record_request)
|
||||
)
|
||||
ignore_hosts = options.get(
|
||||
'ignore_hosts', self.ignore_hosts
|
||||
@@ -145,28 +199,36 @@ class VCR(object):
|
||||
'ignore_localhost', self.ignore_localhost
|
||||
)
|
||||
if filter_headers:
|
||||
filter_functions.append(functools.partial(filters.remove_headers,
|
||||
headers_to_remove=filter_headers))
|
||||
filter_functions.append(
|
||||
functools.partial(
|
||||
filters.remove_headers,
|
||||
headers_to_remove=filter_headers
|
||||
)
|
||||
)
|
||||
if filter_query_parameters:
|
||||
filter_functions.append(functools.partial(filters.remove_query_parameters,
|
||||
query_parameters_to_remove=filter_query_parameters))
|
||||
filter_functions.append(functools.partial(
|
||||
filters.remove_query_parameters,
|
||||
query_parameters_to_remove=filter_query_parameters
|
||||
))
|
||||
if filter_post_data_parameters:
|
||||
filter_functions.append(functools.partial(filters.remove_post_data_parameters,
|
||||
post_data_parameters_to_remove=filter_post_data_parameters))
|
||||
filter_functions.append(
|
||||
functools.partial(
|
||||
filters.remove_post_data_parameters,
|
||||
post_data_parameters_to_remove=filter_post_data_parameters
|
||||
)
|
||||
)
|
||||
|
||||
hosts_to_ignore = list(ignore_hosts)
|
||||
hosts_to_ignore = set(ignore_hosts)
|
||||
if ignore_localhost:
|
||||
hosts_to_ignore.extend(('localhost', '0.0.0.0', '127.0.0.1'))
|
||||
|
||||
hosts_to_ignore.update(('localhost', '0.0.0.0', '127.0.0.1'))
|
||||
if hosts_to_ignore:
|
||||
hosts_to_ignore = set(hosts_to_ignore)
|
||||
filter_functions.append(self._build_ignore_hosts(hosts_to_ignore))
|
||||
|
||||
if before_record_request:
|
||||
if not isinstance(before_record_request, collections.Iterable):
|
||||
before_record_request = (before_record_request,)
|
||||
for function in before_record_request:
|
||||
filter_functions.append(function)
|
||||
filter_functions.extend(before_record_request)
|
||||
|
||||
def before_record_request(request):
|
||||
request = copy.copy(request)
|
||||
for function in filter_functions:
|
||||
@@ -174,7 +236,6 @@ class VCR(object):
|
||||
break
|
||||
request = function(request)
|
||||
return request
|
||||
|
||||
return before_record_request
|
||||
|
||||
@staticmethod
|
||||
@@ -185,8 +246,17 @@ class VCR(object):
|
||||
return request
|
||||
return filter_ignored_hosts
|
||||
|
||||
@staticmethod
|
||||
def _build_path_from_func_using_module(function):
|
||||
return os.path.join(os.path.dirname(inspect.getfile(function)),
|
||||
function.__name__)
|
||||
|
||||
def register_serializer(self, name, serializer):
|
||||
self.serializers[name] = serializer
|
||||
|
||||
def register_matcher(self, name, matcher):
|
||||
self.matchers[name] = matcher
|
||||
|
||||
def test_case(self, predicate=None):
|
||||
predicate = predicate or self.is_test_method
|
||||
return six.with_metaclass(auto_decorate(self.use_cassette, predicate))
|
||||
|
||||
@@ -3,8 +3,5 @@ class CannotOverwriteExistingCassetteException(Exception):
|
||||
|
||||
|
||||
class UnhandledHTTPRequestError(KeyError):
|
||||
'''
|
||||
Raised when a cassette does not c
|
||||
ontain the request we want
|
||||
'''
|
||||
"""Raised when a cassette does not contain the request we want."""
|
||||
pass
|
||||
|
||||
@@ -1,20 +1,16 @@
|
||||
from six import BytesIO
|
||||
from six import BytesIO, text_type
|
||||
from six.moves.urllib.parse import urlparse, urlencode, urlunparse
|
||||
try:
|
||||
from collections import OrderedDict
|
||||
except ImportError:
|
||||
from backport_collections import OrderedDict
|
||||
import copy
|
||||
import json
|
||||
|
||||
from .compat import collections
|
||||
|
||||
|
||||
def remove_headers(request, headers_to_remove):
|
||||
headers = copy.copy(request.headers)
|
||||
headers_to_remove = [h.lower() for h in headers_to_remove]
|
||||
keys = [k for k in headers if k.lower() in headers_to_remove]
|
||||
if keys:
|
||||
for k in keys:
|
||||
headers.pop(k)
|
||||
request.headers = headers
|
||||
new_headers = request.headers.copy()
|
||||
for k in headers_to_remove:
|
||||
if k in new_headers:
|
||||
del new_headers[k]
|
||||
request.headers = new_headers
|
||||
return request
|
||||
|
||||
|
||||
@@ -31,13 +27,23 @@ def remove_query_parameters(request, query_parameters_to_remove):
|
||||
|
||||
def remove_post_data_parameters(request, post_data_parameters_to_remove):
|
||||
if request.method == 'POST' and not isinstance(request.body, BytesIO):
|
||||
post_data = OrderedDict()
|
||||
for k, sep, v in [p.partition(b'=') for p in request.body.split(b'&')]:
|
||||
if k in post_data:
|
||||
post_data[k].append(v)
|
||||
elif len(k) > 0 and k.decode('utf-8') not in post_data_parameters_to_remove:
|
||||
post_data[k] = [v]
|
||||
request.body = b'&'.join(
|
||||
b'='.join([k, v])
|
||||
for k, vals in post_data.items() for v in vals)
|
||||
if request.headers.get('Content-Type') == 'application/json':
|
||||
json_data = json.loads(request.body.decode('utf-8'))
|
||||
for k in list(json_data.keys()):
|
||||
if k in post_data_parameters_to_remove:
|
||||
del json_data[k]
|
||||
request.body = json.dumps(json_data).encode('utf-8')
|
||||
else:
|
||||
post_data = collections.OrderedDict()
|
||||
if isinstance(request.body, text_type):
|
||||
request.body = request.body.encode('utf-8')
|
||||
|
||||
for k, sep, v in (p.partition(b'=') for p in request.body.split(b'&')):
|
||||
if k in post_data:
|
||||
post_data[k].append(v)
|
||||
elif len(k) > 0 and k.decode('utf-8') not in post_data_parameters_to_remove:
|
||||
post_data[k] = [v]
|
||||
request.body = b'&'.join(
|
||||
b'='.join([k, v])
|
||||
for k, vals in post_data.items() for v in vals)
|
||||
return request
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
import json
|
||||
from six.moves import urllib, xmlrpc_client
|
||||
from .util import read_body
|
||||
import logging
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -30,10 +35,49 @@ def query(r1, r2):
|
||||
return r1.query == r2.query
|
||||
|
||||
|
||||
def raw_body(r1, r2):
|
||||
return read_body(r1) == read_body(r2)
|
||||
|
||||
|
||||
def _header_checker(value, header='Content-Type'):
|
||||
def checker(headers):
|
||||
return value in headers.get(header, '').lower()
|
||||
return checker
|
||||
|
||||
|
||||
def _transform_json(body):
|
||||
# Request body is always a byte string, but json.loads() wants a text
|
||||
# string. RFC 7159 says the default encoding is UTF-8 (although UTF-16
|
||||
# and UTF-32 are also allowed: hmmmmm).
|
||||
return json.loads(body.decode('utf-8'))
|
||||
|
||||
|
||||
_xml_header_checker = _header_checker('text/xml')
|
||||
_xmlrpc_header_checker = _header_checker('xmlrpc', header='User-Agent')
|
||||
_checker_transformer_pairs = (
|
||||
(_header_checker('application/x-www-form-urlencoded'), urllib.parse.parse_qs),
|
||||
(_header_checker('application/json'), _transform_json),
|
||||
(lambda request: _xml_header_checker(request) and _xmlrpc_header_checker(request), xmlrpc_client.loads),
|
||||
)
|
||||
|
||||
|
||||
def _identity(x):
|
||||
return x
|
||||
|
||||
|
||||
def _get_transformer(request):
|
||||
for checker, transformer in _checker_transformer_pairs:
|
||||
if checker(request.headers): return transformer
|
||||
else:
|
||||
return _identity
|
||||
|
||||
|
||||
def body(r1, r2):
|
||||
if hasattr(r1.body, 'read') and hasattr(r2.body, 'read'):
|
||||
return r1.body.read() == r2.body.read()
|
||||
return r1.body == r2.body
|
||||
transformer = _get_transformer(r1)
|
||||
r2_transformer = _get_transformer(r2)
|
||||
if transformer != r2_transformer:
|
||||
transformer = _identity
|
||||
return transformer(read_body(r1)) == transformer(read_body(r2))
|
||||
|
||||
|
||||
def headers(r1, r2):
|
||||
|
||||
82
vcr/patch.py
82
vcr/patch.py
@@ -2,9 +2,7 @@
|
||||
import functools
|
||||
import itertools
|
||||
|
||||
import contextlib2
|
||||
import mock
|
||||
|
||||
from .compat import contextlib, mock
|
||||
from .stubs import VCRHTTPConnection, VCRHTTPSConnection
|
||||
from six.moves import http_client as httplib
|
||||
|
||||
@@ -54,6 +52,25 @@ else:
|
||||
_CertValidatingHTTPSConnection = boto.https_connection.CertValidatingHTTPSConnection
|
||||
|
||||
|
||||
# Try to save the original types for Tornado
|
||||
try:
|
||||
import tornado.simple_httpclient
|
||||
except ImportError: # pragma: no cover
|
||||
pass
|
||||
else:
|
||||
_SimpleAsyncHTTPClient_fetch_impl = \
|
||||
tornado.simple_httpclient.SimpleAsyncHTTPClient.fetch_impl
|
||||
|
||||
|
||||
try:
|
||||
import tornado.curl_httpclient
|
||||
except ImportError: # pragma: no cover
|
||||
pass
|
||||
else:
|
||||
_CurlAsyncHTTPClient_fetch_impl = \
|
||||
tornado.curl_httpclient.CurlAsyncHTTPClient.fetch_impl
|
||||
|
||||
|
||||
class CassettePatcherBuilder(object):
|
||||
|
||||
def _build_patchers_from_mock_triples_decorator(function):
|
||||
@@ -70,10 +87,11 @@ class CassettePatcherBuilder(object):
|
||||
|
||||
def build(self):
|
||||
return itertools.chain(
|
||||
self._httplib(), self._requests(), self._urllib3(), self._httplib2(),
|
||||
self._boto(), self._build_patchers_from_mock_triples(
|
||||
self._httplib(), self._requests(), self._urllib3(),
|
||||
self._httplib2(), self._boto(), self._tornado(),
|
||||
self._build_patchers_from_mock_triples(
|
||||
self._cassette.custom_patches
|
||||
)
|
||||
),
|
||||
)
|
||||
|
||||
def _build_patchers_from_mock_triples(self, mock_triples):
|
||||
@@ -206,7 +224,32 @@ class CassettePatcherBuilder(object):
|
||||
else:
|
||||
from .stubs.boto_stubs import VCRCertValidatingHTTPSConnection
|
||||
yield cpool, 'CertValidatingHTTPSConnection', VCRCertValidatingHTTPSConnection
|
||||
|
||||
|
||||
@_build_patchers_from_mock_triples_decorator
|
||||
def _tornado(self):
|
||||
try:
|
||||
import tornado.simple_httpclient as simple
|
||||
except ImportError: # pragma: no cover
|
||||
pass
|
||||
else:
|
||||
from .stubs.tornado_stubs import vcr_fetch_impl
|
||||
|
||||
new_fetch_impl = vcr_fetch_impl(
|
||||
self._cassette, _SimpleAsyncHTTPClient_fetch_impl
|
||||
)
|
||||
yield simple.SimpleAsyncHTTPClient, 'fetch_impl', new_fetch_impl
|
||||
try:
|
||||
import tornado.curl_httpclient as curl
|
||||
except ImportError: # pragma: no cover
|
||||
pass
|
||||
else:
|
||||
from .stubs.tornado_stubs import vcr_fetch_impl
|
||||
|
||||
new_fetch_impl = vcr_fetch_impl(
|
||||
self._cassette, _CurlAsyncHTTPClient_fetch_impl
|
||||
)
|
||||
yield curl.CurlAsyncHTTPClient, 'fetch_impl', new_fetch_impl
|
||||
|
||||
def _urllib3_patchers(self, cpool, stubs):
|
||||
http_connection_remover = ConnectionRemover(
|
||||
self._get_cassette_subclass(stubs.VCRRequestsHTTPConnection)
|
||||
@@ -322,10 +365,31 @@ def reset_patchers():
|
||||
yield mock.patch.object(cpool, 'CertValidatingHTTPSConnection',
|
||||
_CertValidatingHTTPSConnection)
|
||||
|
||||
try:
|
||||
import tornado.simple_httpclient as simple
|
||||
except ImportError: # pragma: no cover
|
||||
pass
|
||||
else:
|
||||
yield mock.patch.object(
|
||||
simple.SimpleAsyncHTTPClient,
|
||||
'fetch_impl',
|
||||
_SimpleAsyncHTTPClient_fetch_impl,
|
||||
)
|
||||
try:
|
||||
import tornado.curl_httpclient as curl
|
||||
except ImportError: # pragma: no cover
|
||||
pass
|
||||
else:
|
||||
yield mock.patch.object(
|
||||
curl.CurlAsyncHTTPClient,
|
||||
'fetch_impl',
|
||||
_CurlAsyncHTTPClient_fetch_impl,
|
||||
)
|
||||
|
||||
@contextlib2.contextmanager
|
||||
|
||||
@contextlib.contextmanager
|
||||
def force_reset():
|
||||
with contextlib2.ExitStack() as exit_stack:
|
||||
with contextlib.ExitStack() as exit_stack:
|
||||
for patcher in reset_patchers():
|
||||
exit_stack.enter_context(patcher)
|
||||
yield
|
||||
|
||||
@@ -1,27 +1,12 @@
|
||||
from six import BytesIO, binary_type
|
||||
import warnings
|
||||
from six import BytesIO, text_type
|
||||
from six.moves.urllib.parse import urlparse, parse_qsl
|
||||
from .util import CaseInsensitiveDict
|
||||
|
||||
|
||||
class Request(object):
|
||||
"""
|
||||
VCR's representation of a request.
|
||||
|
||||
There is a weird quirk in HTTP. You can send the same header twice. For
|
||||
this reason, headers are represented by a dict, with lists as the values.
|
||||
However, it appears that HTTPlib is completely incapable of sending the
|
||||
same header twice. This puts me in a weird position: I want to be able to
|
||||
accurately represent HTTP headers in cassettes, but I don't want the extra
|
||||
step of always having to do [0] in the general case, i.e.
|
||||
request.headers['key'][0]
|
||||
|
||||
In addition, some servers sometimes send the same header more than once,
|
||||
and httplib *can* deal with this situation.
|
||||
|
||||
Futhermore, I wanted to keep the request and response cassette format as
|
||||
similar as possible.
|
||||
|
||||
For this reason, in cassettes I keep a dict with lists as keys, but once
|
||||
deserialized into VCR, I keep them as plain, naked dicts.
|
||||
VCR's representation of a request.
|
||||
"""
|
||||
|
||||
def __init__(self, method, uri, body, headers):
|
||||
@@ -29,14 +14,20 @@ class Request(object):
|
||||
self.uri = uri
|
||||
self._was_file = hasattr(body, 'read')
|
||||
if self._was_file:
|
||||
self._body = body.read()
|
||||
if not isinstance(self._body, binary_type):
|
||||
self._body = self._body.encode('utf-8')
|
||||
self.body = body.read()
|
||||
else:
|
||||
self._body = body
|
||||
self.headers = {}
|
||||
for key in headers:
|
||||
self.add_header(key, headers[key])
|
||||
self.body = body
|
||||
self.headers = headers
|
||||
|
||||
@property
|
||||
def headers(self):
|
||||
return self._headers
|
||||
|
||||
@headers.setter
|
||||
def headers(self, value):
|
||||
if not isinstance(value, HeadersDict):
|
||||
value = HeadersDict(value)
|
||||
self._headers = value
|
||||
|
||||
@property
|
||||
def body(self):
|
||||
@@ -44,14 +35,15 @@ class Request(object):
|
||||
|
||||
@body.setter
|
||||
def body(self, value):
|
||||
if isinstance(value, text_type):
|
||||
value = value.encode('utf-8')
|
||||
self._body = value
|
||||
|
||||
def add_header(self, key, value):
|
||||
# see class docstring for an explanation
|
||||
if isinstance(value, (tuple, list)):
|
||||
self.headers[key] = value[0]
|
||||
else:
|
||||
self.headers[key] = value
|
||||
warnings.warn("Request.add_header is deprecated. "
|
||||
"Please assign to request.headers instead.",
|
||||
DeprecationWarning)
|
||||
self.headers[key] = value
|
||||
|
||||
@property
|
||||
def scheme(self):
|
||||
@@ -105,3 +97,35 @@ class Request(object):
|
||||
@classmethod
|
||||
def _from_dict(cls, dct):
|
||||
return Request(**dct)
|
||||
|
||||
|
||||
class HeadersDict(CaseInsensitiveDict):
|
||||
"""
|
||||
There is a weird quirk in HTTP. You can send the same header twice. For
|
||||
this reason, headers are represented by a dict, with lists as the values.
|
||||
However, it appears that HTTPlib is completely incapable of sending the
|
||||
same header twice. This puts me in a weird position: I want to be able to
|
||||
accurately represent HTTP headers in cassettes, but I don't want the extra
|
||||
step of always having to do [0] in the general case, i.e.
|
||||
request.headers['key'][0]
|
||||
|
||||
In addition, some servers sometimes send the same header more than once,
|
||||
and httplib *can* deal with this situation.
|
||||
|
||||
Futhermore, I wanted to keep the request and response cassette format as
|
||||
similar as possible.
|
||||
|
||||
For this reason, in cassettes I keep a dict with lists as keys, but once
|
||||
deserialized into VCR, I keep them as plain, naked dicts.
|
||||
"""
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
if isinstance(value, (tuple, list)):
|
||||
value = value[0]
|
||||
|
||||
# Preserve the case from the first time this key was set.
|
||||
old = self._store.get(key.lower())
|
||||
if old:
|
||||
key = old[0]
|
||||
|
||||
super(HeadersDict, self).__setitem__(key, value)
|
||||
|
||||
@@ -9,7 +9,6 @@ import six
|
||||
from six.moves.http_client import (
|
||||
HTTPConnection,
|
||||
HTTPSConnection,
|
||||
HTTPMessage,
|
||||
HTTPResponse,
|
||||
)
|
||||
from six import BytesIO
|
||||
@@ -76,6 +75,14 @@ class VCRHTTPResponse(HTTPResponse):
|
||||
self._closed = False
|
||||
|
||||
headers = self.recorded_response['headers']
|
||||
# Since we are loading a response that has already been serialized, our
|
||||
# response is no longer chunked. That means we don't want any
|
||||
# libraries trying to process a chunked response. By removing the
|
||||
# transfer-encoding: chunked header, this should cause the downstream
|
||||
# libraries to process this as a non-chunked response.
|
||||
te_key = [h for h in headers.keys() if h.upper() == 'TRANSFER-ENCODING']
|
||||
if te_key:
|
||||
del headers[te_key[0]]
|
||||
self.headers = self.msg = parse_headers(headers)
|
||||
|
||||
self.length = compat.get_header(self.msg, 'content-length') or None
|
||||
@@ -180,8 +187,7 @@ class VCRConnection(object):
|
||||
log.debug('Got {0}'.format(self._vcr_request))
|
||||
|
||||
def putheader(self, header, *values):
|
||||
for value in values:
|
||||
self._vcr_request.add_header(header, value)
|
||||
self._vcr_request.headers[header] = values
|
||||
|
||||
def send(self, data):
|
||||
'''
|
||||
@@ -196,13 +202,14 @@ class VCRConnection(object):
|
||||
# no need to check that here.
|
||||
self.real_connection.close()
|
||||
|
||||
def endheaders(self, *args, **kwargs):
|
||||
def endheaders(self, message_body=None):
|
||||
"""
|
||||
Normally, this would atually send the request to the server.
|
||||
Normally, this would actually send the request to the server.
|
||||
We are not sending the request until getting the response,
|
||||
so bypass this method for now.
|
||||
so bypass this part and just append the message body, if any.
|
||||
"""
|
||||
pass
|
||||
if message_body is not None:
|
||||
self._vcr_request.body = message_body
|
||||
|
||||
def getresponse(self, _=False, **kwargs):
|
||||
'''Retrieve the response'''
|
||||
@@ -220,12 +227,26 @@ class VCRConnection(object):
|
||||
if self.cassette.write_protected and self.cassette.filter_request(
|
||||
self._vcr_request
|
||||
):
|
||||
most_similar_request = None
|
||||
failing_matchers = None
|
||||
most_similar_request_info = None
|
||||
try:
|
||||
most_similar_request_info = self.cassette.similar_requests(self._vcr_request)
|
||||
most_similar_request = most_similar_request_info[0][0]
|
||||
failing_matchers = self.cassette.failing_matchers(
|
||||
self._vcr_request, most_similar_request
|
||||
)
|
||||
except Exception as err:
|
||||
print "XXXX {0}".format(err)
|
||||
import ipdb; ipdb.set_trace()
|
||||
raise CannotOverwriteExistingCassetteException(
|
||||
"No match for the request (%r) was found. "
|
||||
"Can't overwrite existing cassette (%r) in "
|
||||
"your current record mode (%r)."
|
||||
"your current record mode (%r). Most similar request was (%r). "
|
||||
"It differed from the request according to (%r). \n\n\n(%r)"
|
||||
% (self._vcr_request, self.cassette._path,
|
||||
self.cassette.record_mode)
|
||||
self.cassette.record_mode, most_similar_request,
|
||||
failing_matchers, most_similar_request_info)
|
||||
)
|
||||
|
||||
# Otherwise, we should send the request, then get the response
|
||||
@@ -298,7 +319,7 @@ class VCRConnection(object):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
if six.PY3:
|
||||
kwargs.pop('strict', None) # apparently this is gone in py3
|
||||
kwargs.pop('strict', None) # apparently this is gone in py3
|
||||
|
||||
# need to temporarily reset here because the real connection
|
||||
# inherits from the thing that we are mocking out. Take out
|
||||
@@ -307,6 +328,27 @@ class VCRConnection(object):
|
||||
with force_reset():
|
||||
self.real_connection = self._baseclass(*args, **kwargs)
|
||||
|
||||
def __setattr__(self, name, value):
|
||||
"""
|
||||
We need to define this because any attributes that are set on the
|
||||
VCRConnection need to be propogated to the real connection.
|
||||
|
||||
For example, urllib3 will set certain attributes on the connection,
|
||||
such as 'ssl_version'. These attributes need to get set on the real
|
||||
connection to have the correct and expected behavior.
|
||||
|
||||
TODO: Separately setting the attribute on the two instances is not
|
||||
ideal. We should switch to a proxying implementation.
|
||||
"""
|
||||
try:
|
||||
setattr(self.real_connection, name, value)
|
||||
except AttributeError:
|
||||
# raised if real_connection has not been set yet, such as when
|
||||
# we're setting the real_connection itself for the first time
|
||||
pass
|
||||
|
||||
super(VCRConnection, self).__setattr__(name, value)
|
||||
|
||||
|
||||
class VCRHTTPConnection(VCRConnection):
|
||||
'''A Mocked class for HTTP requests'''
|
||||
|
||||
107
vcr/stubs/tornado_stubs.py
Normal file
107
vcr/stubs/tornado_stubs.py
Normal file
@@ -0,0 +1,107 @@
|
||||
'''Stubs for tornado HTTP clients'''
|
||||
from __future__ import absolute_import
|
||||
|
||||
import functools
|
||||
from six import BytesIO
|
||||
|
||||
from tornado import httputil
|
||||
from tornado.httpclient import HTTPResponse
|
||||
|
||||
from vcr.errors import CannotOverwriteExistingCassetteException
|
||||
from vcr.request import Request
|
||||
|
||||
|
||||
def vcr_fetch_impl(cassette, real_fetch_impl):
|
||||
|
||||
@functools.wraps(real_fetch_impl)
|
||||
def new_fetch_impl(self, request, callback):
|
||||
headers = request.headers.copy()
|
||||
if request.user_agent:
|
||||
headers.setdefault('User-Agent', request.user_agent)
|
||||
|
||||
# TODO body_producer, header_callback, and streaming_callback are not
|
||||
# yet supported.
|
||||
|
||||
unsupported_call = (
|
||||
request.body_producer is not None or
|
||||
request.header_callback is not None or
|
||||
request.streaming_callback is not None
|
||||
)
|
||||
if unsupported_call:
|
||||
response = HTTPResponse(
|
||||
request,
|
||||
599,
|
||||
error=Exception(
|
||||
"The request (%s) uses AsyncHTTPClient functionality "
|
||||
"that is not yet supported by VCR.py. Please make the "
|
||||
"request outside a VCR.py context." % repr(request)
|
||||
),
|
||||
request_time=self.io_loop.time() - request.start_time,
|
||||
)
|
||||
return callback(response)
|
||||
|
||||
vcr_request = Request(
|
||||
request.method,
|
||||
request.url,
|
||||
request.body,
|
||||
headers,
|
||||
)
|
||||
|
||||
if cassette.can_play_response_for(vcr_request):
|
||||
vcr_response = cassette.play_response(vcr_request)
|
||||
headers = httputil.HTTPHeaders()
|
||||
|
||||
recorded_headers = vcr_response['headers']
|
||||
if isinstance(recorded_headers, dict):
|
||||
recorded_headers = recorded_headers.items()
|
||||
for k, vs in recorded_headers:
|
||||
for v in vs:
|
||||
headers.add(k, v)
|
||||
response = HTTPResponse(
|
||||
request,
|
||||
code=vcr_response['status']['code'],
|
||||
reason=vcr_response['status']['message'],
|
||||
headers=headers,
|
||||
buffer=BytesIO(vcr_response['body']['string']),
|
||||
effective_url=vcr_response.get('url'),
|
||||
request_time=self.io_loop.time() - request.start_time,
|
||||
)
|
||||
return callback(response)
|
||||
else:
|
||||
if cassette.write_protected and cassette.filter_request(
|
||||
vcr_request
|
||||
):
|
||||
response = HTTPResponse(
|
||||
request,
|
||||
599,
|
||||
error=CannotOverwriteExistingCassetteException(
|
||||
"No match for the request (%r) was found. "
|
||||
"Can't overwrite existing cassette (%r) in "
|
||||
"your current record mode (%r)."
|
||||
% (vcr_request, cassette._path, cassette.record_mode)
|
||||
),
|
||||
request_time=self.io_loop.time() - request.start_time,
|
||||
)
|
||||
return callback(response)
|
||||
|
||||
def new_callback(response):
|
||||
headers = [
|
||||
(k, response.headers.get_list(k))
|
||||
for k in response.headers.keys()
|
||||
]
|
||||
|
||||
vcr_response = {
|
||||
'status': {
|
||||
'code': response.code,
|
||||
'message': response.reason,
|
||||
},
|
||||
'headers': headers,
|
||||
'body': {'string': response.body},
|
||||
'url': response.effective_url,
|
||||
}
|
||||
cassette.append(vcr_request, vcr_response)
|
||||
return callback(response)
|
||||
|
||||
real_fetch_impl(self, request, new_callback)
|
||||
|
||||
return new_fetch_impl
|
||||
122
vcr/util.py
Normal file
122
vcr/util.py
Normal file
@@ -0,0 +1,122 @@
|
||||
import collections
|
||||
import types
|
||||
|
||||
|
||||
# Shamelessly stolen from https://github.com/kennethreitz/requests/blob/master/requests/structures.py
|
||||
class CaseInsensitiveDict(collections.MutableMapping):
|
||||
"""
|
||||
A case-insensitive ``dict``-like object.
|
||||
Implements all methods and operations of
|
||||
``collections.MutableMapping`` as well as dict's ``copy``. Also
|
||||
provides ``lower_items``.
|
||||
All keys are expected to be strings. The structure remembers the
|
||||
case of the last key to be set, and ``iter(instance)``,
|
||||
``keys()``, ``items()``, ``iterkeys()``, and ``iteritems()``
|
||||
will contain case-sensitive keys. However, querying and contains
|
||||
testing is case insensitive::
|
||||
cid = CaseInsensitiveDict()
|
||||
cid['Accept'] = 'application/json'
|
||||
cid['aCCEPT'] == 'application/json' # True
|
||||
list(cid) == ['Accept'] # True
|
||||
For example, ``headers['content-encoding']`` will return the
|
||||
value of a ``'Content-Encoding'`` response header, regardless
|
||||
of how the header name was originally stored.
|
||||
If the constructor, ``.update``, or equality comparison
|
||||
operations are given keys that have equal ``.lower()``s, the
|
||||
behavior is undefined.
|
||||
"""
|
||||
def __init__(self, data=None, **kwargs):
|
||||
self._store = dict()
|
||||
if data is None:
|
||||
data = {}
|
||||
self.update(data, **kwargs)
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
# Use the lowercased key for lookups, but store the actual
|
||||
# key alongside the value.
|
||||
self._store[key.lower()] = (key, value)
|
||||
|
||||
def __getitem__(self, key):
|
||||
return self._store[key.lower()][1]
|
||||
|
||||
def __delitem__(self, key):
|
||||
del self._store[key.lower()]
|
||||
|
||||
def __iter__(self):
|
||||
return (casedkey for casedkey, mappedvalue in self._store.values())
|
||||
|
||||
def __len__(self):
|
||||
return len(self._store)
|
||||
|
||||
def lower_items(self):
|
||||
"""Like iteritems(), but with all lowercase keys."""
|
||||
return (
|
||||
(lowerkey, keyval[1])
|
||||
for (lowerkey, keyval)
|
||||
in self._store.items()
|
||||
)
|
||||
|
||||
def __eq__(self, other):
|
||||
if isinstance(other, collections.Mapping):
|
||||
other = CaseInsensitiveDict(other)
|
||||
else:
|
||||
return NotImplemented
|
||||
# Compare insensitively
|
||||
return dict(self.lower_items()) == dict(other.lower_items())
|
||||
|
||||
# Copy is required
|
||||
def copy(self):
|
||||
return CaseInsensitiveDict(self._store.values())
|
||||
|
||||
def __repr__(self):
|
||||
return str(dict(self.items()))
|
||||
|
||||
def partition_dict(predicate, dictionary):
|
||||
true_dict = {}
|
||||
false_dict = {}
|
||||
for key, value in dictionary.items():
|
||||
this_dict = true_dict if predicate(key, value) else false_dict
|
||||
this_dict[key] = value
|
||||
return true_dict, false_dict
|
||||
|
||||
|
||||
def compose(*functions):
|
||||
def composed(incoming):
|
||||
res = incoming
|
||||
for function in reversed(functions):
|
||||
if function:
|
||||
res = function(res)
|
||||
return res
|
||||
return composed
|
||||
|
||||
def read_body(request):
|
||||
if hasattr(request.body, 'read'):
|
||||
return request.body.read()
|
||||
return request.body
|
||||
|
||||
|
||||
def auto_decorate(
|
||||
decorator,
|
||||
predicate=lambda name, value: isinstance(value, types.FunctionType)
|
||||
):
|
||||
def maybe_decorate(attribute, value):
|
||||
if predicate(attribute, value):
|
||||
value = decorator(value)
|
||||
return value
|
||||
|
||||
class DecorateAll(type):
|
||||
|
||||
def __setattr__(cls, attribute, value):
|
||||
return super(DecorateAll, cls).__setattr__(
|
||||
attribute, maybe_decorate(attribute, value)
|
||||
)
|
||||
|
||||
def __new__(cls, name, bases, attributes_dict):
|
||||
new_attributes_dict = dict(
|
||||
(attribute, maybe_decorate(attribute, value))
|
||||
for attribute, value in attributes_dict.items()
|
||||
)
|
||||
return super(DecorateAll, cls).__new__(
|
||||
cls, name, bases, new_attributes_dict
|
||||
)
|
||||
return DecorateAll
|
||||
Reference in New Issue
Block a user