mirror of
https://github.com/kevin1024/vcrpy.git
synced 2025-12-08 16:53:23 +00:00
Compare commits
317 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
8bb3c6beee | ||
|
|
df3ad5f35c | ||
|
|
e8a6a7a49f | ||
|
|
881138cb8d | ||
|
|
639dba6f7a | ||
|
|
b9bdc6401d | ||
|
|
3ca5529d26 | ||
|
|
e3f2bc8369 | ||
|
|
fc4e985ee9 | ||
|
|
9038bc9066 | ||
|
|
0def349420 | ||
|
|
0dd7b05990 | ||
|
|
630088599f | ||
|
|
870ab276c4 | ||
|
|
779f3b0474 | ||
|
|
b948ed4857 | ||
|
|
c43e618635 | ||
|
|
5bd40a447a | ||
|
|
4b4be7f661 | ||
|
|
6602a449b1 | ||
|
|
7cd7264034 | ||
|
|
e9c690b9e7 | ||
|
|
bba5df2fbb | ||
|
|
39c3b15e02 | ||
|
|
c87e6d6f6a | ||
|
|
5ab77e22db | ||
|
|
ec6f27bbad | ||
|
|
8930c97ff7 | ||
|
|
e6b43a0374 | ||
|
|
63ec95be06 | ||
|
|
84c45b2742 | ||
|
|
87a25e9ab0 | ||
|
|
2473bdb77a | ||
|
|
32831d4151 | ||
|
|
4991d6f1c8 | ||
|
|
14ef1e87f7 | ||
|
|
fb14739cc1 | ||
|
|
a7c7e4e279 | ||
|
|
c0a22df7ed | ||
|
|
83aed99058 | ||
|
|
e1f65bcbdc | ||
|
|
5301149bd8 | ||
|
|
0297fcdde7 | ||
|
|
9480954c33 | ||
|
|
8432ad32f1 | ||
|
|
fabef3d988 | ||
|
|
da45f46b2d | ||
|
|
562a0ebadc | ||
|
|
ef8ba6d51b | ||
|
|
f6aa6eac84 | ||
|
|
821e148752 | ||
|
|
7306205b8a | ||
|
|
2a128893cc | ||
|
|
5162d183e5 | ||
|
|
9d52c3ed42 | ||
|
|
0e37759175 | ||
|
|
78c6258ba3 | ||
|
|
b047336690 | ||
|
|
c955a5ea88 | ||
|
|
5423d99f5a | ||
|
|
a71c15f398 | ||
|
|
6e049ba7a1 | ||
|
|
916e7839e5 | ||
|
|
99692a92d2 | ||
|
|
a9a68ba44b | ||
|
|
e9f35db405 | ||
|
|
7193407a07 | ||
|
|
c3427ae3a2 | ||
|
|
3a46a6f210 | ||
|
|
163181844b | ||
|
|
2c6f072d11 | ||
|
|
361ed82a10 | ||
|
|
0871c3b87c | ||
|
|
d484dee50f | ||
|
|
b046ee4bb1 | ||
|
|
3dea853482 | ||
|
|
113c95f971 | ||
|
|
a2c947dc48 | ||
|
|
757ad9c836 | ||
|
|
18e5898ec4 | ||
|
|
83211a1887 | ||
|
|
dc249b0965 | ||
|
|
121ed79172 | ||
|
|
b1cdd50e9b | ||
|
|
1018867838 | ||
|
|
b6e96020c1 | ||
|
|
8947f0fc5c | ||
|
|
2bf23b2cdf | ||
|
|
58fcb2b453 | ||
|
|
0c19acd74f | ||
|
|
4868a63876 | ||
|
|
e1e08c7a2c | ||
|
|
5edc58f10c | ||
|
|
2193008150 | ||
|
|
958aac3af3 | ||
|
|
9a564586a4 | ||
|
|
643a4c91ee | ||
|
|
472cc3bffe | ||
|
|
8db46002a3 | ||
|
|
a08c90c5d6 | ||
|
|
8e01426056 | ||
|
|
9a4f5f23a4 | ||
|
|
366e2b75bb | ||
|
|
0cfe63ef6e | ||
|
|
cb05f4163c | ||
|
|
20057a6815 | ||
|
|
0d313502b8 | ||
|
|
d9c2b4b25d | ||
|
|
640681138a | ||
|
|
a02bbbab2b | ||
|
|
f719f90e63 | ||
|
|
3c410b5f9d | ||
|
|
7a5795a547 | ||
|
|
1bd3fbd2c6 | ||
|
|
cd715f37c6 | ||
|
|
9a1147196a | ||
|
|
a23c5d8508 | ||
|
|
868a974900 | ||
|
|
c56de472cd | ||
|
|
c6590f2caf | ||
|
|
70abc5058c | ||
|
|
0c1f1e2479 | ||
|
|
8d90dba16c | ||
|
|
3072c56ed2 | ||
|
|
c84fb1886b | ||
|
|
3b05d499c3 | ||
|
|
8eb54c012f | ||
|
|
6d656717a1 | ||
|
|
d7f7152dbf | ||
|
|
483554ed2e | ||
|
|
83ff73792e | ||
|
|
fd30689c12 | ||
|
|
62f9c528b5 | ||
|
|
007fa851ed | ||
|
|
ffdba74299 | ||
|
|
2c895eb5e9 | ||
|
|
b671e7ab99 | ||
|
|
b36a1157e7 | ||
|
|
0c9761f7ff | ||
|
|
66c6909021 | ||
|
|
c0691a96e6 | ||
|
|
a302874c6d | ||
|
|
b43c63f284 | ||
|
|
2c33ae2664 | ||
|
|
e50f917cf4 | ||
|
|
4ab46f9643 | ||
|
|
23b5d49736 | ||
|
|
bc45a965b2 | ||
|
|
2da49884be | ||
|
|
1e8e7057f5 | ||
|
|
d1a3ab56b1 | ||
|
|
a65da12aeb | ||
|
|
1f99ede46f | ||
|
|
f479f205ad | ||
|
|
c72d19175f | ||
|
|
52c9bf04fe | ||
|
|
bd2121d34e | ||
|
|
f9f2b98427 | ||
|
|
9d5660b673 | ||
|
|
5e295e0603 | ||
|
|
2ef5f9208a | ||
|
|
0be7d6f238 | ||
|
|
3990b32892 | ||
|
|
b5cfd517cf | ||
|
|
adf127c073 | ||
|
|
78f6ce46b5 | ||
|
|
ce5d2225a6 | ||
|
|
3322234b25 | ||
|
|
5d10a38160 | ||
|
|
0b1aeac25e | ||
|
|
1190a0e62e | ||
|
|
61e3bdc402 | ||
|
|
1ff5d08c8b | ||
|
|
1d8e2dbb41 | ||
|
|
62d19e5cc1 | ||
|
|
3d2da26933 | ||
|
|
65c2797f94 | ||
|
|
998dde61ec | ||
|
|
faa83b9aba | ||
|
|
a48f621bae | ||
|
|
fbd5049d38 | ||
|
|
7b253ebc6f | ||
|
|
5d1f35973d | ||
|
|
fbb6382c12 | ||
|
|
eab10578d5 | ||
|
|
e4d1db0617 | ||
|
|
7e677f516d | ||
|
|
34ce0a35ec | ||
|
|
b6195bf41e | ||
|
|
eedafb19ee | ||
|
|
434d6325ea | ||
|
|
25c0141e27 | ||
|
|
1e995c3c9b | ||
|
|
0408bdaadb | ||
|
|
9c9612f93e | ||
|
|
a3eac1f0ec | ||
|
|
710ec6f432 | ||
|
|
9d8426e668 | ||
|
|
424c658da4 | ||
|
|
f0972628ef | ||
|
|
ee28768a31 | ||
|
|
5354ef781c | ||
|
|
750e141b9d | ||
|
|
a042cb3824 | ||
|
|
96d8782d08 | ||
|
|
f9a64e1609 | ||
|
|
2fa1aaa1f7 | ||
|
|
7fe55ad8b8 | ||
|
|
2f6db0dc0c | ||
|
|
4267828a3e | ||
|
|
4e9d5f6885 | ||
|
|
5015dbd878 | ||
|
|
9b188e986f | ||
|
|
6b060e5666 | ||
|
|
bd9fa773e8 | ||
|
|
18ec57fa73 | ||
|
|
6cca703eee | ||
|
|
edf1df9188 | ||
|
|
e0c6a8429d | ||
|
|
08d4d8913a | ||
|
|
792d665893 | ||
|
|
cd32f5114c | ||
|
|
16c6135387 | ||
|
|
5aa2fb017f | ||
|
|
e6fdc735e4 | ||
|
|
f317800cb7 | ||
|
|
4302d7753e | ||
|
|
58ac00a7f6 | ||
|
|
ab27c71a81 | ||
|
|
6273c32334 | ||
|
|
2a072e4dd3 | ||
|
|
582b8eab51 | ||
|
|
e002aab999 | ||
|
|
bc5199c893 | ||
|
|
bd2d2cea72 | ||
|
|
59f3216d44 | ||
|
|
23d1717216 | ||
|
|
1452455d0b | ||
|
|
955e532162 | ||
|
|
20ff2e9d9a | ||
|
|
544d2127d3 | ||
|
|
01901b7a4e | ||
|
|
c83134ca39 | ||
|
|
ebb180f7a5 |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -6,3 +6,5 @@ dist/
|
||||
.coverage
|
||||
*.egg-info/
|
||||
pytestdebug.log
|
||||
|
||||
fixtures/
|
||||
28
.travis.yml
28
.travis.yml
@@ -5,25 +5,47 @@ env:
|
||||
- secure: AifoKzwhjV94cmcQZrdQmqRu/9rkZZvWpwBv1daeAQpLOKFPGsOm3D+x2cSw9+iCfkgDZDfqQVv1kCaFVxTll8v8jTq5SJdqEY0NmGWbj/UkNtShh609oRDsuzLxAEwtVKYjf/h8K2BRea+bl1tGkwZ2vtmYS6dxNlAijjWOfds=
|
||||
- secure: LBSEg/gMj4u4Hrpo3zs6Y/1mTpd2RtcN49mZIFgTdbJ9IhpiNPqcEt647Lz94F9Eses2x2WbNuKqZKZZReY7QLbEzU1m0nN5jlaKrjcG5NR5clNABfFFyhgc0jBikyS4abAG8jc2efeaTrFuQwdoF4sE8YiVrkiVj2X5Xoi6sBk=
|
||||
matrix:
|
||||
- WITH_LIB="requests2.x"
|
||||
- WITH_LIB="requests2.2"
|
||||
- WITH_LIB="requests2.3"
|
||||
- WITH_LIB="requests2.4"
|
||||
- WITH_LIB="requests2.5"
|
||||
- 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"
|
||||
exclude:
|
||||
- env: WITH_LIB="boto"
|
||||
python: 3.3
|
||||
- env: WITH_LIB="boto"
|
||||
python: 3.4
|
||||
- env: WITH_LIB="requests1.x"
|
||||
python: 3.4
|
||||
python:
|
||||
- 2.6
|
||||
- 2.7
|
||||
- 3.3
|
||||
- 3.4
|
||||
- pypy
|
||||
install:
|
||||
- pip install PyYAML pytest --use-mirrors
|
||||
- pip install .
|
||||
- if [ $WITH_LIB = "requests1.x" ] ; then pip install requests==1.2.3; fi
|
||||
- if [ $WITH_LIB = "requests2.x" ] ; then pip install requests; 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.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
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
Copyright (c) 2012-2013 Kevin McCarthy
|
||||
Copyright (c) 2012-2015 Kevin McCarthy
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
|
||||
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 *
|
||||
324
README.md
324
README.md
@@ -1,324 +0,0 @@
|
||||
# VCR.py
|
||||
|
||||

|
||||
|
||||
This is a Python version of [Ruby's VCR library](https://github.com/myronmarston/vcr).
|
||||
|
||||
[](http://travis-ci.org/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, and [pypy](http://pypy.org).
|
||||
|
||||
The following http libraries are supported:
|
||||
|
||||
* urllib2
|
||||
* 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 = ['url', '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 `['url', method']`
|
||||
which means that requests with both the same URL and method (ie POST or
|
||||
GET) are considered identical.
|
||||
|
||||
This can be configured by changing the `match_on` setting.
|
||||
|
||||
The following options are available :
|
||||
|
||||
* method (for example, POST or GET)
|
||||
* url (the full URL, including the protocol)
|
||||
* host (the hostname of the server receiving the request)
|
||||
* path (excluding the hostname)
|
||||
* body (the entire request body)
|
||||
* headers (the headers of the request)
|
||||
|
||||
If these options don't work for you, you can also register your own
|
||||
request matcher. This is described in the Advanced section of this
|
||||
README.
|
||||
|
||||
## Record Modes
|
||||
VCR supports 4 record modes (with the same behavior as Ruby's VCR):
|
||||
|
||||
### 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 url should have been http://www.zombo.com/
|
||||
assert cass.requests[0].url == '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 containing the requests made
|
||||
while this cassette was being used, ordered by the order that the
|
||||
request was made.
|
||||
* `responses`: A list of the responses made.
|
||||
* `play_count`: The number of times this cassette has had a response
|
||||
played back
|
||||
* `responses_of(request)`: Access the responses that match a given request
|
||||
|
||||
The `Request` object has the following properties
|
||||
|
||||
* `URL`: The full url of the request, including the protocol. Example: "http://www.google.com/"
|
||||
* `path`: The path of the request. For example "/" or "/home.html"
|
||||
* `host`: The host of the request, for example "www.google.com"
|
||||
* `port`: The port the request was made on
|
||||
* `method` : The method used to make the request, for example "GET" or "POST"
|
||||
* `protocol`: The protocol used to make the request (http or https)
|
||||
* `body`: The body of the request, usually empty except for POST / PUT / etc
|
||||
|
||||
## 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
|
||||
|
||||
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.url == r2.url and 'JURASSIC PARK' in r1.body
|
||||
|
||||
my_vcr = vcr.VCR()
|
||||
my_vcr.register_matcher('jurassic', jurassic_matcher)
|
||||
|
||||
with my_vcr.use_cassette('test.yml', match_on=['jurassic']):
|
||||
# your http here
|
||||
|
||||
# After you register, you can set the default match_on to use your new matcher
|
||||
|
||||
my_vcr.match_on = ['jurassic']
|
||||
|
||||
with my_vcr.use_cassette('test.yml'):
|
||||
# your http here
|
||||
|
||||
```
|
||||
|
||||
## Installation
|
||||
|
||||
VCR.py is a package on PyPI, so you can `pip install vcrpy` (first you may need to `brew install libyaml` [[Homebrew](http://mxcl.github.com/homebrew/)])
|
||||
|
||||
## Ruby VCR compatibility
|
||||
I'm not trying 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.
|
||||
|
||||
|
||||
## Changelog
|
||||
* 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 bobo 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
|
||||
752
README.rst
Normal file
752
README.rst
Normal file
@@ -0,0 +1,752 @@
|
||||
VCR.py
|
||||
======
|
||||
|
||||
.. figure:: https://raw.github.com/kevin1024/vcrpy/master/vcr.png
|
||||
:alt: vcr.py
|
||||
|
||||
vcr.py
|
||||
|
||||
This is a Python version of `Ruby's VCR
|
||||
library <https://github.com/vcr/vcr>`__.
|
||||
|
||||
|Build Status| |Stories in Ready|
|
||||
|
||||
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 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.
|
||||
|
||||
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.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
|
||||
95
setup.py
95
setup.py
@@ -1,9 +1,13 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
import sys
|
||||
from setuptools import setup
|
||||
from setuptools.command.test import test as TestCommand
|
||||
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):
|
||||
|
||||
@@ -13,42 +17,61 @@ class PyTest(TestCommand):
|
||||
self.test_suite = True
|
||||
|
||||
def run_tests(self):
|
||||
#import here, cause outside the eggs aren't loaded
|
||||
# import here, cause outside the eggs aren't loaded
|
||||
import pytest
|
||||
errno = pytest.main(self.test_args)
|
||||
sys.exit(errno)
|
||||
|
||||
setup(name='vcrpy',
|
||||
version='0.7.0',
|
||||
description="Automatically mock your HTTP interactions to simplify and speed up testing",
|
||||
author='Kevin McCarthy',
|
||||
author_email='me@kevinmccarthy.org',
|
||||
url='https://github.com/kevin1024/vcrpy',
|
||||
packages = [
|
||||
'vcr',
|
||||
'vcr.stubs',
|
||||
'vcr.compat',
|
||||
'vcr.persisters',
|
||||
'vcr.serializers',
|
||||
],
|
||||
package_dir={
|
||||
'vcr': 'vcr',
|
||||
'vcr.stubs': 'vcr/stubs',
|
||||
'vcr.compat': 'vcr/compat',
|
||||
'vcr.persisters': 'vcr/persisters',
|
||||
},
|
||||
install_requires=['PyYAML','contextdecorator','six'],
|
||||
license='MIT',
|
||||
tests_require=['pytest','mock'],
|
||||
cmdclass={'test': PyTest},
|
||||
classifiers=[
|
||||
'Development Status :: 4 - Beta',
|
||||
'Environment :: Console',
|
||||
'Intended Audience :: Developers',
|
||||
'Programming Language :: Python',
|
||||
'Programming Language :: Python :: 3',
|
||||
'Topic :: Software Development :: Testing',
|
||||
'Topic :: Internet :: WWW/HTTP',
|
||||
'License :: OSI Approved :: MIT License',
|
||||
],
|
||||
|
||||
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.7.0',
|
||||
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=install_requires,
|
||||
extras_require=extras_require,
|
||||
license='MIT',
|
||||
tests_require=['pytest', 'mock', 'pytest-localserver'],
|
||||
cmdclass={'test': PyTest},
|
||||
classifiers=[
|
||||
'Development Status :: 4 - Beta',
|
||||
'Environment :: Console',
|
||||
'Intended Audience :: Developers',
|
||||
'Programming Language :: Python',
|
||||
'Programming Language :: Python :: 3',
|
||||
'Topic :: Software Development :: Testing',
|
||||
'Topic :: Internet :: WWW/HTTP',
|
||||
'License :: OSI Approved :: MIT License',
|
||||
]
|
||||
)
|
||||
|
||||
35
tests/fixtures/migration/new_cassette.json
vendored
Normal file
35
tests/fixtures/migration/new_cassette.json
vendored
Normal file
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"version": 1,
|
||||
"interactions":
|
||||
[
|
||||
{
|
||||
"request": {
|
||||
"body": null,
|
||||
"headers": {
|
||||
"accept": ["*/*"],
|
||||
"accept-encoding": ["gzip, deflate, compress"],
|
||||
"user-agent": ["python-requests/2.2.1 CPython/2.6.1 Darwin/10.8.0"]
|
||||
},
|
||||
"method": "GET",
|
||||
"uri": "http://httpbin.org/ip"
|
||||
},
|
||||
"response": {
|
||||
"status": {
|
||||
"message": "OK",
|
||||
"code": 200
|
||||
},
|
||||
"headers": {
|
||||
"access-control-allow-origin": ["*"],
|
||||
"content-type": ["application/json"],
|
||||
"date": ["Mon, 21 Apr 2014 23:13:40 GMT"],
|
||||
"server": ["gunicorn/0.17.4"],
|
||||
"content-length": ["32"],
|
||||
"connection": ["keep-alive"]
|
||||
},
|
||||
"body": {
|
||||
"string": "{\n \"origin\": \"217.122.164.194\"\n}"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
20
tests/fixtures/migration/new_cassette.yaml
vendored
Normal file
20
tests/fixtures/migration/new_cassette.yaml
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
version: 1
|
||||
interactions:
|
||||
- request:
|
||||
body: null
|
||||
headers:
|
||||
accept: ['*/*']
|
||||
accept-encoding: ['gzip, deflate, compress']
|
||||
user-agent: ['python-requests/2.2.1 CPython/2.6.1 Darwin/10.8.0']
|
||||
method: GET
|
||||
uri: http://httpbin.org/ip
|
||||
response:
|
||||
body: {string: !!python/unicode "{\n \"origin\": \"217.122.164.194\"\n}"}
|
||||
headers:
|
||||
access-control-allow-origin: ['*']
|
||||
content-type: [application/json]
|
||||
date: ['Mon, 21 Apr 2014 23:06:09 GMT']
|
||||
server: [gunicorn/0.17.4]
|
||||
content-length: ['32']
|
||||
connection: [keep-alive]
|
||||
status: {code: 200, message: OK}
|
||||
1
tests/fixtures/migration/not_cassette.txt
vendored
Normal file
1
tests/fixtures/migration/not_cassette.txt
vendored
Normal file
@@ -0,0 +1 @@
|
||||
This is not a cassette
|
||||
34
tests/fixtures/migration/old_cassette.json
vendored
Normal file
34
tests/fixtures/migration/old_cassette.json
vendored
Normal file
@@ -0,0 +1,34 @@
|
||||
[
|
||||
{
|
||||
"request": {
|
||||
"body": null,
|
||||
"protocol": "http",
|
||||
"method": "GET",
|
||||
"headers": {
|
||||
"accept-encoding": "gzip, deflate, compress",
|
||||
"accept": "*/*",
|
||||
"user-agent": "python-requests/2.2.1 CPython/2.6.1 Darwin/10.8.0"
|
||||
},
|
||||
"host": "httpbin.org",
|
||||
"path": "/ip",
|
||||
"port": 80
|
||||
},
|
||||
"response": {
|
||||
"status": {
|
||||
"message": "OK",
|
||||
"code": 200
|
||||
},
|
||||
"headers": [
|
||||
"access-control-allow-origin: *\r\n",
|
||||
"content-type: application/json\r\n",
|
||||
"date: Mon, 21 Apr 2014 23:13:40 GMT\r\n",
|
||||
"server: gunicorn/0.17.4\r\n",
|
||||
"content-length: 32\r\n",
|
||||
"connection: keep-alive\r\n"
|
||||
],
|
||||
"body": {
|
||||
"string": "{\n \"origin\": \"217.122.164.194\"\n}"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
18
tests/fixtures/migration/old_cassette.yaml
vendored
Normal file
18
tests/fixtures/migration/old_cassette.yaml
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
- request: !!python/object:vcr.request.Request
|
||||
body: null
|
||||
headers: !!python/object/apply:__builtin__.frozenset
|
||||
- - !!python/tuple [accept-encoding, 'gzip, deflate, compress']
|
||||
- !!python/tuple [user-agent, python-requests/2.2.1 CPython/2.6.1 Darwin/10.8.0]
|
||||
- !!python/tuple [accept, '*/*']
|
||||
host: httpbin.org
|
||||
method: GET
|
||||
path: /ip
|
||||
port: 80
|
||||
protocol: http
|
||||
response:
|
||||
body: {string: !!python/unicode "{\n \"origin\": \"217.122.164.194\"\n}"}
|
||||
headers: [!!python/unicode "access-control-allow-origin: *\r\n", !!python/unicode "content-type:
|
||||
application/json\r\n", !!python/unicode "date: Mon, 21 Apr 2014 23:06:09 GMT\r\n",
|
||||
!!python/unicode "server: gunicorn/0.17.4\r\n", !!python/unicode "content-length:
|
||||
32\r\n", !!python/unicode "connection: keep-alive\r\n"]
|
||||
status: {code: 200, message: OK}
|
||||
61
tests/fixtures/wild/domain_redirect.yaml
vendored
61
tests/fixtures/wild/domain_redirect.yaml
vendored
@@ -1,30 +1,29 @@
|
||||
- request: !!python/object:vcr.request.Request
|
||||
version: 1
|
||||
interactions:
|
||||
- request:
|
||||
body: null
|
||||
headers: !!python/object/apply:__builtin__.frozenset
|
||||
- - !!python/tuple [Accept-Encoding, 'gzip, deflate, compress']
|
||||
- !!python/tuple [User-Agent, vcrpy-test]
|
||||
- !!python/tuple [Accept, '*/*']
|
||||
host: seomoz.org
|
||||
headers:
|
||||
Accept: ['*/*']
|
||||
Accept-Encoding: ['gzip, deflate, compress']
|
||||
User-Agent: ['vcrpy-test']
|
||||
method: GET
|
||||
path: /
|
||||
port: 80
|
||||
protocol: http
|
||||
uri: http://seomoz.org/
|
||||
response:
|
||||
body: {string: ''}
|
||||
headers: ["Location: http://moz.com/\r\n", "Server: BigIP\r\n", "Connection: Keep-Alive\r\n",
|
||||
"Content-Length: 0\r\n"]
|
||||
headers:
|
||||
Location: ['http://moz.com/']
|
||||
Server: ['BigIP']
|
||||
Connection: ['Keep-Alive']
|
||||
Content-Length: ['0']
|
||||
status: {code: 301, message: Moved Permanently}
|
||||
- request: !!python/object:vcr.request.Request
|
||||
- request:
|
||||
body: null
|
||||
headers: !!python/object/apply:__builtin__.frozenset
|
||||
- - !!python/tuple [Accept-Encoding, 'gzip, deflate, compress']
|
||||
- !!python/tuple [User-Agent, vcrpy-test]
|
||||
- !!python/tuple [Accept, '*/*']
|
||||
host: moz.com
|
||||
headers:
|
||||
Accept: ['*/*']
|
||||
Accept-Encoding: ['gzip, deflate, compress']
|
||||
User-Agent: ['vcrpy-test']
|
||||
method: GET
|
||||
path: /
|
||||
port: 80
|
||||
protocol: http
|
||||
uri: http://moz.com/
|
||||
response:
|
||||
body:
|
||||
string: !!binary |
|
||||
@@ -128,10 +127,20 @@
|
||||
HoEMNO6zUAUt4mPnHVJ7KSq318gAt4pmoLKhsMJcIPnZZJjDltyvkuO1tZJBccHl1P9Euv0Pfb4L
|
||||
D7AzkLS64/b2J4QMjTfILZrNZBbH71Ha4KmyJ/eXUEk8H6k0Q79xGEaxvw196olFwXaz9I/+bG63
|
||||
H/31Fxff7Ue851J+//Y3HMaNw24fv2LiZ+n5fxrT+T8T/YhcRkYAAA==
|
||||
headers: ["Server: nginx\r\n", "Content-Type: text/html\r\n", "Vary: Accept-Encoding\r\n",
|
||||
"Cache-Control: no-cache, must-revalidate, s-maxage=3600\r\n", "Expires: Fri,
|
||||
15 Oct 2004 12:00:00 GMT\r\n", "Server-Name: dalmozwww01.dal.moz.com\r\n",
|
||||
"Content-Encoding: gzip\r\n", "Content-Length: 5683\r\n", "Accept-Ranges: bytes\r\n",
|
||||
"Date: Sat, 11 Jan 2014 18:45:11 GMT\r\n", "X-Varnish: 918768771 918700396\r\n",
|
||||
"Age: 3479\r\n", "Via: 1.1 varnish\r\n", "Connection: keep-alive\r\n"]
|
||||
headers:
|
||||
Server: [nginx]
|
||||
Content-Type: [text/html]
|
||||
Vary: [Accept-Encoding]
|
||||
Cache-Control: [no-cache]
|
||||
must-revalidate: ['s-maxage=3600']
|
||||
Expires: ['Fri 15 Oct 2004 12:00:00 GMT']
|
||||
Server-Name: ['dalmozwww01.dal.moz.com']
|
||||
Content-Encoding: [gzip]
|
||||
Content-Length: ['5683']
|
||||
Accept-Ranges: [bytes]
|
||||
Date: ['Sat, 11 Jan 2014 18:45:11 GMT']
|
||||
X-Varnish: [918768771 918700396]
|
||||
Age: ['3479']
|
||||
Via: ['1.1 varnish']
|
||||
Connection: [keep-alive]
|
||||
status: {code: 200, message: OK}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
'''Basic tests about cassettes'''
|
||||
# coding=utf-8
|
||||
# -*- coding: utf-8 -*-
|
||||
'''Basic tests for cassettes'''
|
||||
|
||||
# External imports
|
||||
import os
|
||||
|
||||
@@ -1,9 +1,22 @@
|
||||
import pytest
|
||||
boto = pytest.importorskip("boto")
|
||||
import boto
|
||||
import boto.iam
|
||||
from boto.s3.connection import S3Connection
|
||||
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
|
||||
# CertValidatingHTTPSConnection refers to the patched version.
|
||||
from boto.https_connection import CertValidatingHTTPSConnection
|
||||
from vcr.stubs.boto_stubs import VCRCertValidatingHTTPSConnection
|
||||
# Prove that the class was patched by the stub and that we can instantiate it.
|
||||
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
|
||||
@@ -39,3 +52,21 @@ def test_boto_hardcore_mode(tmpdir):
|
||||
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')
|
||||
except DuplicateSectionError:
|
||||
pass
|
||||
# Ensure that boto uses HTTPS
|
||||
boto.config.set('Boto', 'is_secure', 'true')
|
||||
# Ensure that boto uses CertValidatingHTTPSConnection
|
||||
boto.config.set('Boto', 'https_validate_certificates', 'true')
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join('boto-iam.yml'))) as cass:
|
||||
iam_conn = boto.iam.connect_to_region('universal')
|
||||
iam_conn.get_all_users()
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join('boto-iam.yml'))) as cass:
|
||||
iam_conn = boto.iam.connect_to_region('universal')
|
||||
iam_conn.get_all_users()
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
'''Basic tests about save behavior'''
|
||||
# coding=utf-8
|
||||
|
||||
# External imports
|
||||
import os
|
||||
|
||||
97
tests/integration/test_filter.py
Normal file
97
tests/integration/test_filter.py
Normal file
@@ -0,0 +1,97 @@
|
||||
import base64
|
||||
import pytest
|
||||
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):
|
||||
request = Request(url)
|
||||
base64string = base64.b64encode(
|
||||
username.encode('ascii') + b':' + password.encode('ascii')
|
||||
)
|
||||
request.add_header(b"Authorization", b"Basic " + base64string)
|
||||
return urlopen(request)
|
||||
|
||||
|
||||
def _find_header(cassette, header):
|
||||
for request in cassette.requests:
|
||||
for k in request.headers:
|
||||
if header.lower() == k.lower():
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def test_filter_basic_auth(tmpdir):
|
||||
url = 'http://httpbin.org/basic-auth/user/passwd'
|
||||
cass_file = str(tmpdir.join('basic_auth_filter.yaml'))
|
||||
my_vcr = vcr.VCR(match_on=['uri', 'method', 'headers'])
|
||||
# 2 requests, one with auth failure and one with auth success
|
||||
with my_vcr.use_cassette(cass_file, filter_headers=['authorization']):
|
||||
with pytest.raises(HTTPError):
|
||||
resp = _request_with_auth(url, 'user', 'wrongpasswd')
|
||||
assert resp.getcode() == 401
|
||||
resp = _request_with_auth(url, 'user', 'passwd')
|
||||
assert resp.getcode() == 200
|
||||
# make same 2 requests, this time both served from cassette.
|
||||
with my_vcr.use_cassette(cass_file, filter_headers=['authorization']) as cass:
|
||||
with pytest.raises(HTTPError):
|
||||
resp = _request_with_auth(url, 'user', 'wrongpasswd')
|
||||
assert resp.getcode() == 401
|
||||
resp = _request_with_auth(url, 'user', 'passwd')
|
||||
assert resp.getcode() == 200
|
||||
# authorization header should not have been recorded
|
||||
assert not _find_header(cass, 'authorization')
|
||||
assert len(cass) == 2
|
||||
|
||||
|
||||
def test_filter_querystring(tmpdir):
|
||||
url = 'http://httpbin.org/?foo=bar'
|
||||
cass_file = str(tmpdir.join('filter_qs.yaml'))
|
||||
with vcr.use_cassette(cass_file, filter_query_parameters=['foo']):
|
||||
urlopen(url)
|
||||
with vcr.use_cassette(cass_file, filter_query_parameters=['foo']) as cass:
|
||||
urlopen(url)
|
||||
assert 'foo' not in cass.requests[0].url
|
||||
|
||||
|
||||
def test_filter_post_data(tmpdir):
|
||||
url = 'http://httpbin.org/post'
|
||||
data = urlencode({'id': 'secret', 'foo': 'bar'}).encode('utf-8')
|
||||
cass_file = str(tmpdir.join('filter_pd.yaml'))
|
||||
with vcr.use_cassette(cass_file, filter_post_data_parameters=['id']):
|
||||
urlopen(url, data)
|
||||
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_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'))
|
||||
def before_record_cb(request):
|
||||
if request.path != '/get':
|
||||
return request
|
||||
# Test the legacy keyword.
|
||||
my_vcr = vcr.VCR(before_record=before_record_cb)
|
||||
with my_vcr.use_cassette(cass_file, filter_headers=['authorization']) as cass:
|
||||
urlopen(url)
|
||||
assert len(cass) == 0
|
||||
|
||||
my_vcr = vcr.VCR(before_record_request=before_record_cb)
|
||||
with my_vcr.use_cassette(cass_file, filter_headers=['authorization']) as cass:
|
||||
urlopen(url)
|
||||
assert len(cass) == 0
|
||||
@@ -1,5 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
'''Integration tests with httplib2'''
|
||||
# coding=utf-8
|
||||
|
||||
# External imports
|
||||
from six.moves.urllib_parse import urlencode
|
||||
@@ -54,7 +54,7 @@ def test_response_headers(scheme, tmpdir):
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join('headers.yaml'))) as cass:
|
||||
resp, _ = httplib2.Http().request(url)
|
||||
assert headers == resp.items()
|
||||
assert set(headers) == set(resp.items())
|
||||
|
||||
|
||||
def test_multiple_requests(scheme, tmpdir):
|
||||
|
||||
55
tests/integration/test_ignore.py
Normal file
55
tests/integration/test_ignore.py
Normal file
@@ -0,0 +1,55 @@
|
||||
import base64
|
||||
import pytest
|
||||
from six.moves.urllib.request import urlopen, Request
|
||||
from six.moves.urllib.error import HTTPError
|
||||
import vcr
|
||||
|
||||
|
||||
def test_ignore_localhost(tmpdir, httpserver):
|
||||
httpserver.serve_content('Hello!')
|
||||
cass_file = str(tmpdir.join('filter_qs.yaml'))
|
||||
with vcr.use_cassette(cass_file, ignore_localhost=True) as cass:
|
||||
urlopen(httpserver.url)
|
||||
assert len(cass) == 0
|
||||
urlopen('http://httpbin.org')
|
||||
assert len(cass) == 1
|
||||
|
||||
|
||||
def test_ignore_httpbin(tmpdir, httpserver):
|
||||
httpserver.serve_content('Hello!')
|
||||
cass_file = str(tmpdir.join('filter_qs.yaml'))
|
||||
with vcr.use_cassette(
|
||||
cass_file,
|
||||
ignore_hosts=['httpbin.org']
|
||||
) as cass:
|
||||
urlopen('http://httpbin.org')
|
||||
assert len(cass) == 0
|
||||
urlopen(httpserver.url)
|
||||
assert len(cass) == 1
|
||||
|
||||
|
||||
def test_ignore_localhost_and_httpbin(tmpdir, httpserver):
|
||||
httpserver.serve_content('Hello!')
|
||||
cass_file = str(tmpdir.join('filter_qs.yaml'))
|
||||
with vcr.use_cassette(
|
||||
cass_file,
|
||||
ignore_hosts=['httpbin.org'],
|
||||
ignore_localhost=True
|
||||
) as cass:
|
||||
urlopen('http://httpbin.org')
|
||||
urlopen(httpserver.url)
|
||||
assert len(cass) == 0
|
||||
|
||||
def test_ignore_localhost_twice(tmpdir, httpserver):
|
||||
httpserver.serve_content('Hello!')
|
||||
cass_file = str(tmpdir.join('filter_qs.yaml'))
|
||||
with vcr.use_cassette(cass_file, ignore_localhost=True) as cass:
|
||||
urlopen(httpserver.url)
|
||||
assert len(cass) == 0
|
||||
urlopen('http://httpbin.org')
|
||||
assert len(cass) == 1
|
||||
with vcr.use_cassette(cass_file, ignore_localhost=True) as cass:
|
||||
assert len(cass) == 1
|
||||
urlopen(httpserver.url)
|
||||
urlopen('http://httpbin.org')
|
||||
assert len(cass) == 1
|
||||
99
tests/integration/test_matchers.py
Normal file
99
tests/integration/test_matchers.py
Normal file
@@ -0,0 +1,99 @@
|
||||
import vcr
|
||||
import pytest
|
||||
from six.moves.urllib.request import urlopen
|
||||
|
||||
|
||||
DEFAULT_URI = 'http://httpbin.org/get?p1=q1&p2=q2' # base uri for testing
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def cassette(tmpdir):
|
||||
"""
|
||||
Helper fixture used to prepare the cassete
|
||||
returns path to the recorded cassette
|
||||
"""
|
||||
cassette_path = str(tmpdir.join('test.yml'))
|
||||
with vcr.use_cassette(cassette_path, record_mode='all'):
|
||||
urlopen(DEFAULT_URI)
|
||||
return cassette_path
|
||||
|
||||
|
||||
@pytest.mark.parametrize("matcher, matching_uri, not_matching_uri", [
|
||||
('uri',
|
||||
'http://httpbin.org/get?p1=q1&p2=q2',
|
||||
'http://httpbin.org/get?p2=q2&p1=q1'),
|
||||
('scheme',
|
||||
'http://google.com/post?a=b',
|
||||
'https://httpbin.org/get?p1=q1&p2=q2'),
|
||||
('host',
|
||||
'https://httpbin.org/post?a=b',
|
||||
'http://google.com/get?p1=q1&p2=q2'),
|
||||
('port',
|
||||
'https://google.com:80/post?a=b',
|
||||
'http://httpbin.org:5000/get?p1=q1&p2=q2'),
|
||||
('path',
|
||||
'https://google.com/get?a=b',
|
||||
'http://httpbin.org/post?p1=q1&p2=q2'),
|
||||
('query',
|
||||
'https://google.com/get?p2=q2&p1=q1',
|
||||
'http://httpbin.org/get?p1=q1&a=b')
|
||||
])
|
||||
def test_matchers(cassette, matcher, matching_uri, not_matching_uri):
|
||||
# play cassette with default uri
|
||||
with vcr.use_cassette(cassette, match_on=[matcher]) as cass:
|
||||
urlopen(DEFAULT_URI)
|
||||
assert cass.play_count == 1
|
||||
|
||||
# play cassette with matching on uri
|
||||
with vcr.use_cassette(cassette, match_on=[matcher]) as cass:
|
||||
urlopen(matching_uri)
|
||||
assert cass.play_count == 1
|
||||
|
||||
# play cassette with not matching on uri, it should fail
|
||||
with pytest.raises(vcr.errors.CannotOverwriteExistingCassetteException):
|
||||
with vcr.use_cassette(cassette, match_on=[matcher]) as cass:
|
||||
urlopen(not_matching_uri)
|
||||
|
||||
|
||||
def test_method_matcher(cassette):
|
||||
# play cassette with matching on method
|
||||
with vcr.use_cassette(cassette, match_on=['method']) as cass:
|
||||
urlopen('https://google.com/get?a=b')
|
||||
assert cass.play_count == 1
|
||||
|
||||
# should fail if method does not match
|
||||
with pytest.raises(vcr.errors.CannotOverwriteExistingCassetteException):
|
||||
with vcr.use_cassette(cassette, match_on=['method']) as cass:
|
||||
# is a POST request
|
||||
urlopen(DEFAULT_URI, data=b'')
|
||||
|
||||
|
||||
@pytest.mark.parametrize("uri", [
|
||||
DEFAULT_URI,
|
||||
'http://httpbin.org/get?p2=q2&p1=q1',
|
||||
'http://httpbin.org/get?p2=q2&p1=q1',
|
||||
])
|
||||
def test_default_matcher_matches(cassette, uri):
|
||||
with vcr.use_cassette(cassette) as cass:
|
||||
urlopen(uri)
|
||||
assert cass.play_count == 1
|
||||
|
||||
|
||||
@pytest.mark.parametrize("uri", [
|
||||
'https://httpbin.org/get?p1=q1&p2=q2',
|
||||
'http://google.com/get?p1=q1&p2=q2',
|
||||
'http://httpbin.org:5000/get?p1=q1&p2=q2',
|
||||
'http://httpbin.org/post?p1=q1&p2=q2',
|
||||
'http://httpbin.org/get?p1=q1&a=b'
|
||||
])
|
||||
def test_default_matcher_does_not_match(cassette, uri):
|
||||
with pytest.raises(vcr.errors.CannotOverwriteExistingCassetteException):
|
||||
with vcr.use_cassette(cassette):
|
||||
urlopen(uri)
|
||||
|
||||
|
||||
def test_default_matcher_does_not_match_on_method(cassette):
|
||||
with pytest.raises(vcr.errors.CannotOverwriteExistingCassetteException):
|
||||
with vcr.use_cassette(cassette):
|
||||
# is a POST request
|
||||
urlopen(DEFAULT_URI, data=b'')
|
||||
@@ -54,13 +54,48 @@ def test_new_episodes_record_mode(tmpdir):
|
||||
# make the same request again
|
||||
response = urlopen('http://httpbin.org/').read()
|
||||
|
||||
# all responses have been played
|
||||
assert cass.all_played
|
||||
|
||||
# in the "new_episodes" record mode, we can add more requests to
|
||||
# a cassette without repurcussions.
|
||||
response = urlopen('http://httpbin.org/get').read()
|
||||
|
||||
# the first interaction was not re-recorded, but the second was added
|
||||
# one of the responses has been played
|
||||
assert cass.play_count == 1
|
||||
|
||||
# not all responses have been played
|
||||
assert not cass.all_played
|
||||
|
||||
with vcr.use_cassette(testfile, record_mode="new_episodes") as cass:
|
||||
# the cassette should now have 2 responses
|
||||
assert len(cass.responses) == 2
|
||||
|
||||
|
||||
def test_new_episodes_record_mode_two_times(tmpdir):
|
||||
testfile = str(tmpdir.join('recordmode.yml'))
|
||||
url = 'http://httpbin.org/bytes/1024'
|
||||
with vcr.use_cassette(testfile, record_mode="new_episodes"):
|
||||
# cassette file doesn't exist, so create.
|
||||
original_first_response = urlopen(url).read()
|
||||
|
||||
with vcr.use_cassette(testfile, record_mode="new_episodes"):
|
||||
# make the same request again
|
||||
assert urlopen(url).read() == original_first_response
|
||||
|
||||
# in the "new_episodes" record mode, we can add the same request
|
||||
# to the cassette without repercussions
|
||||
original_second_response = urlopen(url).read()
|
||||
|
||||
with vcr.use_cassette(testfile, record_mode="once"):
|
||||
# make the same request again
|
||||
assert urlopen(url).read() == original_first_response
|
||||
assert urlopen(url).read() == original_second_response
|
||||
# now that we are back in once mode, this should raise
|
||||
# an error.
|
||||
with pytest.raises(Exception):
|
||||
urlopen(url).read()
|
||||
|
||||
|
||||
def test_all_record_mode(tmpdir):
|
||||
testfile = str(tmpdir.join('recordmode.yml'))
|
||||
|
||||
@@ -10,7 +10,7 @@ def false_matcher(r1, r2):
|
||||
return False
|
||||
|
||||
|
||||
def test_registered_serializer_true_matcher(tmpdir):
|
||||
def test_registered_true_matcher(tmpdir):
|
||||
my_vcr = vcr.VCR()
|
||||
my_vcr.register_matcher('true', true_matcher)
|
||||
testfile = str(tmpdir.join('test.yml'))
|
||||
@@ -25,7 +25,7 @@ def test_registered_serializer_true_matcher(tmpdir):
|
||||
urlopen('https://httpbin.org/get')
|
||||
|
||||
|
||||
def test_registered_serializer_false_matcher(tmpdir):
|
||||
def test_registered_false_matcher(tmpdir):
|
||||
my_vcr = vcr.VCR()
|
||||
my_vcr.register_matcher('false', false_matcher)
|
||||
testfile = str(tmpdir.join('test.yml'))
|
||||
|
||||
@@ -10,7 +10,7 @@ class MockSerializer(object):
|
||||
def deserialize(self, cassette_string):
|
||||
self.serialize_count += 1
|
||||
self.cassette_string = cassette_string
|
||||
return ([], [])
|
||||
return {'interactions':[]}
|
||||
|
||||
def serialize(self, cassette_dict):
|
||||
self.deserialize_count += 1
|
||||
|
||||
@@ -2,10 +2,21 @@ import vcr
|
||||
from six.moves.urllib.request import urlopen
|
||||
|
||||
|
||||
def test_recorded_request_url_with_redirected_request(tmpdir):
|
||||
def test_recorded_request_uri_with_redirected_request(tmpdir):
|
||||
with vcr.use_cassette(str(tmpdir.join('test.yml'))) as cass:
|
||||
assert len(cass) == 0
|
||||
urlopen('http://httpbin.org/redirect/3')
|
||||
assert cass.requests[0].url == 'http://httpbin.org/redirect/3'
|
||||
assert cass.requests[3].url == 'http://httpbin.org/get'
|
||||
assert cass.requests[0].uri == 'http://httpbin.org/redirect/3'
|
||||
assert cass.requests[3].uri == 'http://httpbin.org/get'
|
||||
assert len(cass) == 4
|
||||
|
||||
|
||||
def test_records_multiple_header_values(tmpdir, httpserver):
|
||||
httpserver.serve_content('Hello!', headers=[('foo', 'bar'), ('foo', 'baz')])
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join('test.yml'))) as cass:
|
||||
assert len(cass) == 0
|
||||
|
||||
urlopen(httpserver.url)
|
||||
assert len(cass) == 1
|
||||
assert cass.responses[0]['headers']['foo'] == ['bar', 'baz']
|
||||
|
||||
@@ -1,53 +1,47 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
'''Test requests' interaction with vcr'''
|
||||
|
||||
# coding=utf-8
|
||||
|
||||
import os
|
||||
import pytest
|
||||
import vcr
|
||||
from assertions import (
|
||||
assert_cassette_empty,
|
||||
assert_cassette_has_one_response,
|
||||
assert_is_json
|
||||
)
|
||||
from assertions import assert_cassette_empty, assert_is_json
|
||||
|
||||
|
||||
requests = pytest.importorskip("requests")
|
||||
|
||||
|
||||
@pytest.fixture(params=["https", "http"])
|
||||
def scheme(request):
|
||||
"""
|
||||
Fixture that returns both http and https
|
||||
"""
|
||||
'''Fixture that returns both http and https.'''
|
||||
return request.param
|
||||
|
||||
|
||||
def test_status_code(scheme, tmpdir):
|
||||
'''Ensure that we can read the status code'''
|
||||
url = scheme + '://httpbin.org/'
|
||||
with vcr.use_cassette(str(tmpdir.join('atts.yaml'))) as cass:
|
||||
with vcr.use_cassette(str(tmpdir.join('atts.yaml'))):
|
||||
status_code = requests.get(url).status_code
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join('atts.yaml'))) as cass:
|
||||
with vcr.use_cassette(str(tmpdir.join('atts.yaml'))):
|
||||
assert status_code == requests.get(url).status_code
|
||||
|
||||
|
||||
def test_headers(scheme, tmpdir):
|
||||
'''Ensure that we can read the headers back'''
|
||||
url = scheme + '://httpbin.org/'
|
||||
with vcr.use_cassette(str(tmpdir.join('headers.yaml'))) as cass:
|
||||
with vcr.use_cassette(str(tmpdir.join('headers.yaml'))):
|
||||
headers = requests.get(url).headers
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join('headers.yaml'))) as cass:
|
||||
with vcr.use_cassette(str(tmpdir.join('headers.yaml'))):
|
||||
assert headers == requests.get(url).headers
|
||||
|
||||
|
||||
def test_body(tmpdir, scheme):
|
||||
'''Ensure the responses are all identical enough'''
|
||||
url = scheme + '://httpbin.org/bytes/1024'
|
||||
with vcr.use_cassette(str(tmpdir.join('body.yaml'))) as cass:
|
||||
with vcr.use_cassette(str(tmpdir.join('body.yaml'))):
|
||||
content = requests.get(url).content
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join('body.yaml'))) as cass:
|
||||
with vcr.use_cassette(str(tmpdir.join('body.yaml'))):
|
||||
assert content == requests.get(url).content
|
||||
|
||||
|
||||
@@ -55,10 +49,10 @@ def test_auth(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'))) as cass:
|
||||
with vcr.use_cassette(str(tmpdir.join('auth.yaml'))):
|
||||
one = requests.get(url, auth=auth)
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join('auth.yaml'))) as cass:
|
||||
with vcr.use_cassette(str(tmpdir.join('auth.yaml'))):
|
||||
two = requests.get(url, auth=auth)
|
||||
assert one.content == two.content
|
||||
assert one.status_code == two.status_code
|
||||
@@ -81,10 +75,10 @@ def test_post(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'))) as cass:
|
||||
with vcr.use_cassette(str(tmpdir.join('requests.yaml'))):
|
||||
req1 = requests.post(url, data).content
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join('requests.yaml'))) as cass:
|
||||
with vcr.use_cassette(str(tmpdir.join('requests.yaml'))):
|
||||
req2 = requests.post(url, data).content
|
||||
|
||||
assert req1 == req2
|
||||
@@ -93,7 +87,7 @@ def test_post(tmpdir, scheme):
|
||||
def test_redirects(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'))) as cass:
|
||||
with vcr.use_cassette(str(tmpdir.join('requests.yaml'))):
|
||||
content = requests.get(url).content
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join('requests.yaml'))) as cass:
|
||||
@@ -124,11 +118,11 @@ def test_gzip(tmpdir, scheme):
|
||||
url = scheme + '://httpbin.org/gzip'
|
||||
response = requests.get(url)
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join('gzip.yaml'))) as cass:
|
||||
with vcr.use_cassette(str(tmpdir.join('gzip.yaml'))):
|
||||
response = requests.get(url)
|
||||
assert_is_json(response.content)
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join('gzip.yaml'))) as cass:
|
||||
with vcr.use_cassette(str(tmpdir.join('gzip.yaml'))):
|
||||
assert_is_json(response.content)
|
||||
|
||||
|
||||
@@ -143,5 +137,100 @@ def test_session_and_connection_close(tmpdir, scheme):
|
||||
with vcr.use_cassette(str(tmpdir.join('session_connection_closed.yaml'))):
|
||||
session = requests.session()
|
||||
|
||||
resp = session.get('http://httpbin.org/get', headers={'Connection': 'close'})
|
||||
resp = session.get('http://httpbin.org/get', headers={'Connection': 'close'})
|
||||
session.get('http://httpbin.org/get', headers={'Connection': 'close'})
|
||||
session.get('http://httpbin.org/get', headers={'Connection': 'close'})
|
||||
|
||||
|
||||
def test_https_with_cert_validation_disabled(tmpdir):
|
||||
with vcr.use_cassette(str(tmpdir.join('cert_validation_disabled.yaml'))):
|
||||
requests.get('https://httpbin.org', verify=False)
|
||||
|
||||
|
||||
def test_session_can_make_requests_after_requests_unpatched(tmpdir):
|
||||
with vcr.use_cassette(str(tmpdir.join('test_session_after_unpatched.yaml'))):
|
||||
session = requests.session()
|
||||
session.get('http://httpbin.org/get')
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join('test_session_after_unpatched.yaml'))):
|
||||
session = requests.session()
|
||||
session.get('http://httpbin.org/get')
|
||||
|
||||
session.get('http://httpbin.org/status/200')
|
||||
|
||||
|
||||
def test_session_created_before_use_cassette_is_patched(tmpdir, scheme):
|
||||
url = scheme + '://httpbin.org/bytes/1024'
|
||||
# Record arbitrary, random data to the cassette
|
||||
with vcr.use_cassette(str(tmpdir.join('session_created_outside.yaml'))):
|
||||
session = requests.session()
|
||||
body = session.get(url).content
|
||||
|
||||
# Create a session outside of any cassette context manager
|
||||
session = requests.session()
|
||||
# Make a request to make sure that a connectionpool is instantiated
|
||||
session.get(scheme + '://httpbin.org/get')
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join('session_created_outside.yaml'))):
|
||||
# These should only be the same if the patching succeeded.
|
||||
assert session.get(url).content == body
|
||||
|
||||
|
||||
def test_nested_cassettes_with_session_created_before_nesting(scheme, tmpdir):
|
||||
'''
|
||||
This tests ensures that a session that was created while one cassette was
|
||||
active is patched to the use the responses of a second cassette when it
|
||||
is enabled.
|
||||
'''
|
||||
url = scheme + '://httpbin.org/bytes/1024'
|
||||
with vcr.use_cassette(str(tmpdir.join('first_nested.yaml'))):
|
||||
session = requests.session()
|
||||
first_body = session.get(url).content
|
||||
with vcr.use_cassette(str(tmpdir.join('second_nested.yaml'))):
|
||||
second_body = session.get(url).content
|
||||
third_body = requests.get(url).content
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join('second_nested.yaml'))):
|
||||
session = requests.session()
|
||||
assert session.get(url).content == second_body
|
||||
with vcr.use_cassette(str(tmpdir.join('first_nested.yaml'))):
|
||||
assert session.get(url).content == first_body
|
||||
assert session.get(url).content == third_body
|
||||
|
||||
# Make sure that the session can now get content normally.
|
||||
session.get('http://www.reddit.com')
|
||||
|
||||
|
||||
def test_post_file(tmpdir, scheme):
|
||||
'''Ensure that we handle posting a file.'''
|
||||
url = scheme + '://httpbin.org/post'
|
||||
with vcr.use_cassette(str(tmpdir.join('post_file.yaml'))) as cass:
|
||||
# Don't use 2.7+ only style ',' separated with here because we support python 2.6
|
||||
with open('tox.ini') as f:
|
||||
original_response = requests.post(url, f).content
|
||||
|
||||
# This also tests that we do the right thing with matching the body when they are files.
|
||||
with vcr.use_cassette(str(tmpdir.join('post_file.yaml')),
|
||||
match_on=('method', 'scheme', 'host', 'port', 'path', 'query', 'body')) as cass:
|
||||
with open('tox.ini', 'rb') as f:
|
||||
tox_content = f.read()
|
||||
assert cass.requests[0].body.read() == tox_content
|
||||
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
|
||||
|
||||
|
||||
42
tests/integration/test_stubs.py
Normal file
42
tests/integration/test_stubs.py
Normal file
@@ -0,0 +1,42 @@
|
||||
import vcr
|
||||
import six.moves.http_client as httplib
|
||||
|
||||
def _headers_are_case_insensitive():
|
||||
conn = httplib.HTTPConnection('httpbin.org')
|
||||
conn.request('GET', "/cookies/set?k1=v1")
|
||||
r1 = conn.getresponse()
|
||||
cookie_data1 = r1.getheader('set-cookie')
|
||||
conn = httplib.HTTPConnection('httpbin.org')
|
||||
conn.request('GET', "/cookies/set?k1=v1")
|
||||
r2 = conn.getresponse()
|
||||
cookie_data2 = r2.getheader('Set-Cookie')
|
||||
return cookie_data1 == cookie_data2
|
||||
|
||||
def test_case_insensitivity(tmpdir):
|
||||
testfile = str(tmpdir.join('case_insensitivity.yml'))
|
||||
# check if headers are case insensitive outside of vcrpy
|
||||
outside = _headers_are_case_insensitive()
|
||||
with vcr.use_cassette(testfile):
|
||||
# check if headers are case insensitive inside of vcrpy
|
||||
inside = _headers_are_case_insensitive()
|
||||
# check if headers are case insensitive after vcrpy deserializes headers
|
||||
inside2 = _headers_are_case_insensitive()
|
||||
|
||||
# behavior should be the same both inside and outside
|
||||
assert outside == inside == inside2
|
||||
|
||||
def _multiple_header_value(httpserver):
|
||||
conn = httplib.HTTPConnection('%s:%s' % httpserver.server_address)
|
||||
conn.request('GET', "/")
|
||||
r = conn.getresponse()
|
||||
return r.getheader('foo')
|
||||
|
||||
def test_multiple_headers(tmpdir, httpserver):
|
||||
testfile = str(tmpdir.join('multiple_headers.yaml'))
|
||||
httpserver.serve_content('Hello!', headers=[('foo', 'bar'), ('foo', 'baz')])
|
||||
outside = _multiple_header_value(httpserver)
|
||||
|
||||
with vcr.use_cassette(testfile):
|
||||
inside = _multiple_header_value(httpserver)
|
||||
|
||||
assert outside == inside
|
||||
300
tests/integration/test_tornado.py
Normal file
300
tests/integration/test_tornado.py
Normal file
@@ -0,0 +1,300 @@
|
||||
# -*- 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_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
|
||||
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
|
||||
@@ -1,8 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
'''Integration tests with urllib2'''
|
||||
# coding=utf-8
|
||||
|
||||
# External imports
|
||||
import os
|
||||
|
||||
import pytest
|
||||
from six.moves.urllib.request import urlopen
|
||||
@@ -11,7 +8,7 @@ from six.moves.urllib_parse import urlencode
|
||||
# Internal imports
|
||||
import vcr
|
||||
|
||||
from assertions import assert_cassette_empty, assert_cassette_has_one_response
|
||||
from assertions import assert_cassette_has_one_response
|
||||
|
||||
|
||||
@pytest.fixture(params=["https", "http"])
|
||||
@@ -50,7 +47,7 @@ def test_response_headers(scheme, tmpdir):
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join('headers.yaml'))) as cass:
|
||||
open2 = urlopen(url).info().items()
|
||||
assert open1 == open2
|
||||
assert sorted(open1) == sorted(open2)
|
||||
|
||||
|
||||
def test_multiple_requests(scheme, tmpdir):
|
||||
|
||||
148
tests/integration/test_urllib3.py
Normal file
148
tests/integration/test_urllib3.py
Normal file
@@ -0,0 +1,148 @@
|
||||
'''Integration tests with urllib3'''
|
||||
|
||||
# coding=utf-8
|
||||
|
||||
import pytest
|
||||
import vcr
|
||||
from assertions import assert_cassette_empty, assert_is_json
|
||||
certifi = pytest.importorskip("certifi")
|
||||
urllib3 = pytest.importorskip("urllib3")
|
||||
|
||||
|
||||
@pytest.fixture(params=["https", "http"])
|
||||
def scheme(request):
|
||||
"""
|
||||
Fixture that returns both http and https
|
||||
"""
|
||||
return request.param
|
||||
|
||||
|
||||
@pytest.fixture(scope='module')
|
||||
def verify_pool_mgr():
|
||||
return urllib3.PoolManager(
|
||||
cert_reqs='CERT_REQUIRED', # Force certificate check.
|
||||
ca_certs=certifi.where()
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(scope='module')
|
||||
def pool_mgr():
|
||||
return urllib3.PoolManager()
|
||||
|
||||
|
||||
def test_status_code(scheme, tmpdir, verify_pool_mgr):
|
||||
'''Ensure that we can read the status code'''
|
||||
url = scheme + '://httpbin.org/'
|
||||
with vcr.use_cassette(str(tmpdir.join('atts.yaml'))):
|
||||
status_code = verify_pool_mgr.request('GET', url).status
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join('atts.yaml'))):
|
||||
assert status_code == verify_pool_mgr.request('GET', url).status
|
||||
|
||||
|
||||
def test_headers(scheme, tmpdir, verify_pool_mgr):
|
||||
'''Ensure that we can read the headers back'''
|
||||
url = scheme + '://httpbin.org/'
|
||||
with vcr.use_cassette(str(tmpdir.join('headers.yaml'))):
|
||||
headers = verify_pool_mgr.request('GET', url).headers
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join('headers.yaml'))):
|
||||
assert headers == verify_pool_mgr.request('GET', url).headers
|
||||
|
||||
|
||||
def test_body(tmpdir, scheme, verify_pool_mgr):
|
||||
'''Ensure the responses are all identical enough'''
|
||||
url = scheme + '://httpbin.org/bytes/1024'
|
||||
with vcr.use_cassette(str(tmpdir.join('body.yaml'))):
|
||||
content = verify_pool_mgr.request('GET', url).data
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join('body.yaml'))):
|
||||
assert content == verify_pool_mgr.request('GET', url).data
|
||||
|
||||
|
||||
def test_auth(tmpdir, scheme, verify_pool_mgr):
|
||||
'''Ensure that we can handle basic auth'''
|
||||
auth = ('user', 'passwd')
|
||||
headers = urllib3.util.make_headers(basic_auth='{0}:{1}'.format(*auth))
|
||||
url = scheme + '://httpbin.org/basic-auth/user/passwd'
|
||||
with vcr.use_cassette(str(tmpdir.join('auth.yaml'))):
|
||||
one = verify_pool_mgr.request('GET', url, headers=headers)
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join('auth.yaml'))):
|
||||
two = verify_pool_mgr.request('GET', url, headers=headers)
|
||||
assert one.data == two.data
|
||||
assert one.status == two.status
|
||||
|
||||
|
||||
def test_auth_failed(tmpdir, scheme, verify_pool_mgr):
|
||||
'''Ensure that we can save failed auth statuses'''
|
||||
auth = ('user', 'wrongwrongwrong')
|
||||
headers = urllib3.util.make_headers(basic_auth='{0}:{1}'.format(*auth))
|
||||
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 = verify_pool_mgr.request('GET', url, headers=headers)
|
||||
two = verify_pool_mgr.request('GET', url, headers=headers)
|
||||
assert one.data == two.data
|
||||
assert one.status == two.status == 401
|
||||
|
||||
|
||||
def test_post(tmpdir, scheme, verify_pool_mgr):
|
||||
'''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('verify_pool_mgr.yaml'))):
|
||||
req1 = verify_pool_mgr.request('POST', url, data).data
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join('verify_pool_mgr.yaml'))):
|
||||
req2 = verify_pool_mgr.request('POST', url, data).data
|
||||
|
||||
assert req1 == req2
|
||||
|
||||
|
||||
def test_redirects(tmpdir, scheme, verify_pool_mgr):
|
||||
'''Ensure that we can handle redirects'''
|
||||
url = scheme + '://httpbin.org/redirect-to?url=bytes/1024'
|
||||
with vcr.use_cassette(str(tmpdir.join('verify_pool_mgr.yaml'))):
|
||||
content = verify_pool_mgr.request('GET', url).data
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join('verify_pool_mgr.yaml'))) as cass:
|
||||
assert content == verify_pool_mgr.request('GET', url).data
|
||||
# Ensure that we've now cached *two* responses. One for the redirect
|
||||
# and one for the final fetch
|
||||
assert len(cass) == 2
|
||||
assert cass.play_count == 2
|
||||
|
||||
|
||||
def test_cross_scheme(tmpdir, scheme, verify_pool_mgr):
|
||||
'''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:
|
||||
verify_pool_mgr.request('GET', 'https://httpbin.org/')
|
||||
verify_pool_mgr.request('GET', 'http://httpbin.org/')
|
||||
assert cass.play_count == 0
|
||||
assert len(cass) == 2
|
||||
|
||||
|
||||
def test_gzip(tmpdir, scheme, verify_pool_mgr):
|
||||
'''
|
||||
Ensure that requests (actually urllib3) is able to automatically decompress
|
||||
the response body
|
||||
'''
|
||||
url = scheme + '://httpbin.org/gzip'
|
||||
response = verify_pool_mgr.request('GET', url)
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join('gzip.yaml'))):
|
||||
response = verify_pool_mgr.request('GET', url)
|
||||
assert_is_json(response.data)
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join('gzip.yaml'))):
|
||||
assert_is_json(response.data)
|
||||
|
||||
|
||||
def test_https_with_cert_validation_disabled(tmpdir, pool_mgr):
|
||||
with vcr.use_cassette(str(tmpdir.join('cert_validation_disabled.yaml'))):
|
||||
pool_mgr.request('GET', 'https://httpbin.org')
|
||||
@@ -1,4 +1,6 @@
|
||||
import pytest
|
||||
from six.moves import xmlrpc_client
|
||||
|
||||
requests = pytest.importorskip("requests")
|
||||
|
||||
import vcr
|
||||
@@ -40,11 +42,11 @@ def test_flickr_multipart_upload():
|
||||
data = r.read()
|
||||
h.close()
|
||||
|
||||
with vcr.use_cassette('fixtures/vcr_cassettes/flickr.json') as cass:
|
||||
with vcr.use_cassette('fixtures/vcr_cassettes/flickr.yaml') as cass:
|
||||
_pretend_to_be_flickr_library()
|
||||
assert len(cass) == 1
|
||||
|
||||
with vcr.use_cassette('fixtures/vcr_cassettes/flickr.json') as cass:
|
||||
with vcr.use_cassette('fixtures/vcr_cassettes/flickr.yaml') as cass:
|
||||
assert len(cass) == 1
|
||||
_pretend_to_be_flickr_library()
|
||||
assert cass.play_count == 1
|
||||
@@ -56,12 +58,6 @@ def test_flickr_should_respond_with_200(tmpdir):
|
||||
r = requests.post("http://api.flickr.com/services/upload")
|
||||
assert r.status_code == 200
|
||||
|
||||
def x_test_zip_file(tmpdir):
|
||||
# TODO: How do I make this pass?
|
||||
zipfile = "http://www.colorado.edu/conflict/peace/download/peace_example.ZIP"
|
||||
testfile = str(tmpdir.join('test.json'))
|
||||
with vcr.use_cassette(testfile, serializer='json'):
|
||||
r = requests.post(zipfile)
|
||||
|
||||
def test_cookies(tmpdir):
|
||||
testfile = str(tmpdir.join('cookies.yml'))
|
||||
@@ -72,3 +68,22 @@ def test_cookies(tmpdir):
|
||||
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,16 +1,25 @@
|
||||
import copy
|
||||
import inspect
|
||||
import os
|
||||
|
||||
from six.moves import http_client as httplib
|
||||
import pytest
|
||||
import yaml
|
||||
import mock
|
||||
|
||||
from vcr.compat import mock, contextlib
|
||||
from vcr.cassette import Cassette
|
||||
from vcr.errors import UnhandledHTTPRequestError
|
||||
from vcr.patch import force_reset
|
||||
from vcr.stubs import VCRHTTPSConnection
|
||||
|
||||
|
||||
def test_cassette_load(tmpdir):
|
||||
a_file = tmpdir.join('test_cassette.yml')
|
||||
a_file.write(yaml.dump([
|
||||
{'request': 'foo', 'response': 'bar'}
|
||||
]))
|
||||
a_cassette = Cassette.load(str(a_file))
|
||||
a_file.write(yaml.dump({'interactions': [
|
||||
{'request': {'body': '', 'uri': 'foo', 'method': 'GET', 'headers': {}},
|
||||
'response': 'bar'}
|
||||
]}))
|
||||
a_cassette = Cassette.load(path=str(a_file))
|
||||
assert len(a_cassette) == 1
|
||||
|
||||
|
||||
@@ -57,10 +66,224 @@ def test_cassette_get_missing_response():
|
||||
with pytest.raises(UnhandledHTTPRequestError):
|
||||
a.responses_of('foo')
|
||||
|
||||
|
||||
@mock.patch('vcr.cassette.requests_match', _mock_requests_match)
|
||||
def test_cassette_cant_read_same_request_twice():
|
||||
a = Cassette('test')
|
||||
a.append('foo','bar')
|
||||
a.append('foo', 'bar')
|
||||
a.play_response('foo')
|
||||
with pytest.raises(UnhandledHTTPRequestError):
|
||||
a.play_response('foo')
|
||||
|
||||
|
||||
def make_get_request():
|
||||
conn = httplib.HTTPConnection("www.python.org")
|
||||
conn.request("GET", "/index.html")
|
||||
return conn.getresponse()
|
||||
|
||||
|
||||
@mock.patch('vcr.cassette.requests_match', return_value=True)
|
||||
@mock.patch('vcr.cassette.load_cassette', lambda *args, **kwargs: (('foo',), (mock.MagicMock(),)))
|
||||
@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(path='test')(make_get_request)
|
||||
for i in range(4):
|
||||
decorated_function()
|
||||
|
||||
|
||||
def test_arg_getter_functionality():
|
||||
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 = {'path': 'other'}
|
||||
|
||||
with context_decorator as cassette:
|
||||
assert cassette._path == 'other'
|
||||
|
||||
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:
|
||||
function()
|
||||
cassette_load.assert_called_once_with(**arg_getter.return_value)
|
||||
|
||||
|
||||
def test_cassette_not_all_played():
|
||||
a = Cassette('test')
|
||||
a.append('foo', 'bar')
|
||||
assert not a.all_played
|
||||
|
||||
|
||||
@mock.patch('vcr.cassette.requests_match', _mock_requests_match)
|
||||
def test_cassette_all_played():
|
||||
a = Cassette('test')
|
||||
a.append('foo', 'bar')
|
||||
a.play_response('foo')
|
||||
assert a.all_played
|
||||
|
||||
|
||||
def test_before_record_response():
|
||||
before_record_response = mock.Mock(return_value='mutated')
|
||||
cassette = Cassette('test', before_record_response=before_record_response)
|
||||
cassette.append('req', 'res')
|
||||
|
||||
before_record_response.assert_called_once_with('res')
|
||||
assert cassette.responses[0] == 'mutated'
|
||||
|
||||
|
||||
def assert_get_response_body_is(value):
|
||||
conn = httplib.HTTPConnection("www.python.org")
|
||||
conn.request("GET", "/index.html")
|
||||
assert conn.getresponse().read().decode('utf8') == value
|
||||
|
||||
|
||||
@mock.patch('vcr.cassette.requests_match', _mock_requests_match)
|
||||
@mock.patch('vcr.cassette.Cassette.can_play_response_for', return_value=True)
|
||||
@mock.patch('vcr.cassette.Cassette._save', return_value=True)
|
||||
def test_nesting_cassette_context_managers(*args):
|
||||
first_response = {'body': {'string': b'first_response'}, 'headers': {},
|
||||
'status': {'message': 'm', 'code': 200}}
|
||||
|
||||
second_response = copy.deepcopy(first_response)
|
||||
second_response['body']['string'] = b'second_response'
|
||||
|
||||
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(path='test') as second_cassette:
|
||||
with mock.patch.object(second_cassette, 'play_response', return_value=second_response):
|
||||
assert_get_response_body_is('second_response')
|
||||
|
||||
# Now the first cassette should be back in effect
|
||||
assert_get_response_body_is('first_response')
|
||||
|
||||
|
||||
def test_nesting_context_managers_by_checking_references_of_http_connection():
|
||||
original = httplib.HTTPConnection
|
||||
with Cassette.use(path='test'):
|
||||
first_cassette_HTTPConnection = httplib.HTTPConnection
|
||||
with Cassette.use(path='test'):
|
||||
second_cassette_HTTPConnection = httplib.HTTPConnection
|
||||
assert second_cassette_HTTPConnection is not first_cassette_HTTPConnection
|
||||
with Cassette.use(path='test'):
|
||||
assert httplib.HTTPConnection is not second_cassette_HTTPConnection
|
||||
with force_reset():
|
||||
assert httplib.HTTPConnection is original
|
||||
assert httplib.HTTPConnection is second_cassette_HTTPConnection
|
||||
assert httplib.HTTPConnection is first_cassette_HTTPConnection
|
||||
|
||||
|
||||
def test_custom_patchers():
|
||||
class Test(object):
|
||||
attribute = None
|
||||
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(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
|
||||
|
||||
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_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]
|
||||
|
||||
96
tests/unit/test_filters.py
Normal file
96
tests/unit/test_filters.py
Normal file
@@ -0,0 +1,96 @@
|
||||
from vcr.filters import (
|
||||
remove_headers,
|
||||
remove_query_parameters,
|
||||
remove_post_data_parameters
|
||||
)
|
||||
from vcr.request import Request
|
||||
import json
|
||||
|
||||
|
||||
def test_remove_headers():
|
||||
headers = {'hello': ['goodbye'], 'secret': ['header']}
|
||||
request = Request('GET', 'http://google.com', '', headers)
|
||||
remove_headers(request, ['secret'])
|
||||
assert request.headers == {'hello': 'goodbye'}
|
||||
|
||||
|
||||
def test_remove_headers_empty():
|
||||
headers = {'hello': 'goodbye', 'secret': 'header'}
|
||||
request = Request('GET', 'http://google.com', '', headers)
|
||||
remove_headers(request, [])
|
||||
assert request.headers == headers
|
||||
|
||||
|
||||
def test_remove_query_parameters():
|
||||
uri = 'http://g.com/?q=cowboys&w=1'
|
||||
request = Request('GET', uri, '', {})
|
||||
remove_query_parameters(request, ['w'])
|
||||
assert request.uri == 'http://g.com/?q=cowboys'
|
||||
|
||||
|
||||
def test_remove_all_query_parameters():
|
||||
uri = 'http://g.com/?q=cowboys&w=1'
|
||||
request = Request('GET', uri, '', {})
|
||||
remove_query_parameters(request, ['w', 'q'])
|
||||
assert request.uri == 'http://g.com/'
|
||||
|
||||
|
||||
def test_remove_nonexistent_query_parameters():
|
||||
uri = 'http://g.com/'
|
||||
request = Request('GET', uri, '', {})
|
||||
remove_query_parameters(request, ['w', 'q'])
|
||||
assert request.uri == 'http://g.com/'
|
||||
|
||||
|
||||
def test_remove_post_data_parameters():
|
||||
body = b'id=secret&foo=bar'
|
||||
request = Request('POST', 'http://google.com', body, {})
|
||||
remove_post_data_parameters(request, ['id'])
|
||||
assert request.body == b'foo=bar'
|
||||
|
||||
|
||||
def test_preserve_multiple_post_data_parameters():
|
||||
body = b'id=secret&foo=bar&foo=baz'
|
||||
request = Request('POST', 'http://google.com', body, {})
|
||||
remove_post_data_parameters(request, ['id'])
|
||||
assert request.body == b'foo=bar&foo=baz'
|
||||
|
||||
|
||||
def test_remove_all_post_data_parameters():
|
||||
body = b'id=secret&foo=bar'
|
||||
request = Request('POST', 'http://google.com', body, {})
|
||||
remove_post_data_parameters(request, ['id', 'foo'])
|
||||
assert request.body == b''
|
||||
|
||||
|
||||
def test_remove_nonexistent_post_data_parameters():
|
||||
body = b''
|
||||
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.add_header('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.add_header('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.add_header('Content-Type', 'application/json')
|
||||
remove_post_data_parameters(request, ['id'])
|
||||
assert request.body == b'{}'
|
||||
19
tests/unit/test_json_serializer.py
Normal file
19
tests/unit/test_json_serializer.py
Normal file
@@ -0,0 +1,19 @@
|
||||
import pytest
|
||||
from vcr.serializers.jsonserializer import serialize
|
||||
from vcr.request import Request
|
||||
|
||||
|
||||
def test_serialize_binary():
|
||||
request = Request(
|
||||
method='GET',
|
||||
uri='http://localhost/',
|
||||
body='',
|
||||
headers={},
|
||||
)
|
||||
cassette = {'requests': [request], 'responses': [{'body': b'\x8c'}]}
|
||||
|
||||
with pytest.raises(Exception) as e:
|
||||
serialize(cassette)
|
||||
assert e.message == "Error serializing cassette to JSON. Does this \
|
||||
HTTP interaction contain binary data? If so, use a different \
|
||||
serializer (like the yaml serializer) for this request"
|
||||
159
tests/unit/test_matchers.py
Normal file
159
tests/unit/test_matchers.py
Normal file
@@ -0,0 +1,159 @@
|
||||
import itertools
|
||||
|
||||
import pytest
|
||||
|
||||
from vcr import matchers
|
||||
from vcr import request
|
||||
|
||||
# the dict contains requests with corresponding to its key difference
|
||||
# with 'base' request.
|
||||
REQUESTS = {
|
||||
'base': request.Request('GET', 'http://host.com/p?a=b', '', {}),
|
||||
'method': request.Request('POST', 'http://host.com/p?a=b', '', {}),
|
||||
'scheme': request.Request('GET', 'https://host.com:80/p?a=b', '', {}),
|
||||
'host': request.Request('GET', 'http://another-host.com/p?a=b', '', {}),
|
||||
'port': request.Request('GET', 'http://host.com:90/p?a=b', '', {}),
|
||||
'path': request.Request('GET', 'http://host.com/x?a=b', '', {}),
|
||||
'query': request.Request('GET', 'http://host.com/p?c=d', '', {}),
|
||||
}
|
||||
|
||||
|
||||
def assert_matcher(matcher_name):
|
||||
matcher = getattr(matchers, matcher_name)
|
||||
for k1, k2 in itertools.permutations(REQUESTS, 2):
|
||||
matched = matcher(REQUESTS[k1], REQUESTS[k2])
|
||||
if matcher_name in set((k1, k2)):
|
||||
assert not matched
|
||||
else:
|
||||
assert matched
|
||||
|
||||
|
||||
def test_uri_matcher():
|
||||
for k1, k2 in itertools.permutations(REQUESTS, 2):
|
||||
matched = matchers.uri(REQUESTS[k1], REQUESTS[k2])
|
||||
if set((k1, k2)) != set(('base', 'method')):
|
||||
assert not matched
|
||||
else:
|
||||
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', '', {})
|
||||
assert matchers.query(req1, req2)
|
||||
|
||||
req1 = request.Request('GET', 'http://host.com/?a=b&a=b&c=d', '', {})
|
||||
req2 = request.Request('GET', 'http://host.com/?a=b&c=d&a=b', '', {})
|
||||
req3 = request.Request('GET', 'http://host.com/?c=d&a=b&a=b', '', {})
|
||||
assert matchers.query(req1, req2)
|
||||
assert matchers.query(req1, req3)
|
||||
|
||||
|
||||
def test_metchers():
|
||||
assert_matcher('method')
|
||||
assert_matcher('scheme')
|
||||
assert_matcher('host')
|
||||
assert_matcher('port')
|
||||
assert_matcher('path')
|
||||
assert_matcher('query')
|
||||
41
tests/unit/test_migration.py
Normal file
41
tests/unit/test_migration.py
Normal file
@@ -0,0 +1,41 @@
|
||||
import filecmp
|
||||
import json
|
||||
import shutil
|
||||
import yaml
|
||||
|
||||
import vcr.migration
|
||||
|
||||
|
||||
def test_try_migrate_with_json(tmpdir):
|
||||
cassette = tmpdir.join('cassette.json').strpath
|
||||
shutil.copy('tests/fixtures/migration/old_cassette.json', cassette)
|
||||
assert vcr.migration.try_migrate(cassette)
|
||||
with open('tests/fixtures/migration/new_cassette.json', 'r') as f:
|
||||
expected_json = json.load(f)
|
||||
with open(cassette, 'r') as f:
|
||||
actual_json = json.load(f)
|
||||
assert actual_json == expected_json
|
||||
|
||||
|
||||
def test_try_migrate_with_yaml(tmpdir):
|
||||
cassette = tmpdir.join('cassette.yaml').strpath
|
||||
shutil.copy('tests/fixtures/migration/old_cassette.yaml', cassette)
|
||||
assert vcr.migration.try_migrate(cassette)
|
||||
with open('tests/fixtures/migration/new_cassette.yaml', 'r') as f:
|
||||
expected_yaml = yaml.load(f)
|
||||
with open(cassette, 'r') as f:
|
||||
actual_yaml = yaml.load(f)
|
||||
assert actual_yaml == expected_yaml
|
||||
|
||||
|
||||
def test_try_migrate_with_invalid_or_new_cassettes(tmpdir):
|
||||
cassette = tmpdir.join('cassette').strpath
|
||||
files = [
|
||||
'tests/fixtures/migration/not_cassette.txt',
|
||||
'tests/fixtures/migration/new_cassette.yaml',
|
||||
'tests/fixtures/migration/new_cassette.json',
|
||||
]
|
||||
for file_path in files:
|
||||
shutil.copy(file_path, cassette)
|
||||
assert not vcr.migration.try_migrate(cassette)
|
||||
assert filecmp.cmp(cassette, file_path) # shold not change file
|
||||
24
tests/unit/test_persist.py
Normal file
24
tests/unit/test_persist.py
Normal file
@@ -0,0 +1,24 @@
|
||||
import pytest
|
||||
|
||||
import vcr.persist
|
||||
from vcr.serializers import jsonserializer, yamlserializer
|
||||
|
||||
|
||||
@pytest.mark.parametrize("cassette_path, serializer", [
|
||||
('tests/fixtures/migration/old_cassette.json', jsonserializer),
|
||||
('tests/fixtures/migration/old_cassette.yaml', yamlserializer),
|
||||
])
|
||||
def test_load_cassette_with_old_cassettes(cassette_path, serializer):
|
||||
with pytest.raises(ValueError) as excinfo:
|
||||
vcr.persist.load_cassette(cassette_path, serializer)
|
||||
assert "run the migration script" in excinfo.exconly()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("cassette_path, serializer", [
|
||||
('tests/fixtures/migration/not_cassette.txt', jsonserializer),
|
||||
('tests/fixtures/migration/not_cassette.txt', yamlserializer),
|
||||
])
|
||||
def test_load_cassette_with_invalid_cassettes(cassette_path, serializer):
|
||||
with pytest.raises(Exception) as excinfo:
|
||||
vcr.persist.load_cassette(cassette_path, serializer)
|
||||
assert "run the migration script" not in excinfo.exconly()
|
||||
@@ -1,11 +1,38 @@
|
||||
import pytest
|
||||
|
||||
from vcr.request import Request
|
||||
|
||||
|
||||
def test_url():
|
||||
req = Request('http', 'www.google.com', 80, 'GET', '/', '', {})
|
||||
assert req.url == 'http://www.google.com/'
|
||||
|
||||
|
||||
def test_str():
|
||||
req = Request('http', 'www.google.com', 80, 'GET', '/', '', {})
|
||||
str(req) == '<Request (GET) http://www.google.com>'
|
||||
req = Request('GET', 'http://www.google.com/', '', {})
|
||||
str(req) == '<Request (GET) http://www.google.com/>'
|
||||
|
||||
|
||||
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')
|
||||
assert req.headers == {'X-Header1': 'h11', 'X-Header2': 'h2'}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("uri, expected_port", [
|
||||
('http://go.com/', 80),
|
||||
('http://go.com:80/', 80),
|
||||
('http://go.com:3000/', 3000),
|
||||
('https://go.com/', 443),
|
||||
('https://go.com:443/', 443),
|
||||
('https://go.com:3000/', 3000),
|
||||
])
|
||||
def test_port(uri, expected_port):
|
||||
req = Request('GET', uri, '', {})
|
||||
assert req.port == expected_port
|
||||
|
||||
|
||||
def test_uri():
|
||||
req = Request('GET', 'http://go.com/', '', {})
|
||||
assert req.uri == 'http://go.com/'
|
||||
|
||||
req = Request('GET', 'http://go.com:80/', '', {})
|
||||
assert req.uri == 'http://go.com:80/'
|
||||
|
||||
68
tests/unit/test_response.py
Normal file
68
tests/unit/test_response.py
Normal file
@@ -0,0 +1,68 @@
|
||||
# coding: UTF-8
|
||||
from vcr.stubs import VCRHTTPResponse
|
||||
|
||||
|
||||
def test_response_should_have_headers_field():
|
||||
recorded_response = {
|
||||
"status": {
|
||||
"message": "OK",
|
||||
"code": 200
|
||||
},
|
||||
"headers": {
|
||||
"content-length": ["0"],
|
||||
"server": ["gunicorn/18.0"],
|
||||
"connection": ["Close"],
|
||||
"access-control-allow-credentials": ["true"],
|
||||
"date": ["Fri, 24 Oct 2014 18:35:37 GMT"],
|
||||
"access-control-allow-origin": ["*"],
|
||||
"content-type": ["text/html; charset=utf-8"],
|
||||
},
|
||||
"body": {
|
||||
"string": b""
|
||||
}
|
||||
}
|
||||
response = VCRHTTPResponse(recorded_response)
|
||||
|
||||
assert response.headers is not None
|
||||
|
||||
|
||||
def test_response_headers_should_be_equal_to_msg():
|
||||
recorded_response = {
|
||||
"status": {
|
||||
"message": b"OK",
|
||||
"code": 200
|
||||
},
|
||||
"headers": {
|
||||
"content-length": ["0"],
|
||||
"server": ["gunicorn/18.0"],
|
||||
"connection": ["Close"],
|
||||
"content-type": ["text/html; charset=utf-8"],
|
||||
},
|
||||
"body": {
|
||||
"string": b""
|
||||
}
|
||||
}
|
||||
response = VCRHTTPResponse(recorded_response)
|
||||
|
||||
assert response.headers == response.msg
|
||||
|
||||
|
||||
def test_response_headers_should_have_correct_values():
|
||||
recorded_response = {
|
||||
"status": {
|
||||
"message": "OK",
|
||||
"code": 200
|
||||
},
|
||||
"headers": {
|
||||
"content-length": ["10806"],
|
||||
"date": ["Fri, 24 Oct 2014 18:35:37 GMT"],
|
||||
"content-type": ["text/html; charset=utf-8"],
|
||||
},
|
||||
"body": {
|
||||
"string": b""
|
||||
}
|
||||
}
|
||||
response = VCRHTTPResponse(recorded_response)
|
||||
|
||||
assert response.headers.get('content-length') == "10806"
|
||||
assert response.headers.get('date') == "Fri, 24 Oct 2014 18:35:37 GMT"
|
||||
85
tests/unit/test_serialize.py
Normal file
85
tests/unit/test_serialize.py
Normal file
@@ -0,0 +1,85 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
import pytest
|
||||
|
||||
from vcr.compat import mock
|
||||
from vcr.serialize import deserialize
|
||||
from vcr.serializers import yamlserializer, jsonserializer
|
||||
|
||||
|
||||
def test_deserialize_old_yaml_cassette():
|
||||
with open('tests/fixtures/migration/old_cassette.yaml', 'r') as f:
|
||||
with pytest.raises(ValueError):
|
||||
deserialize(f.read(), yamlserializer)
|
||||
|
||||
|
||||
def test_deserialize_old_json_cassette():
|
||||
with open('tests/fixtures/migration/old_cassette.json', 'r') as f:
|
||||
with pytest.raises(ValueError):
|
||||
deserialize(f.read(), jsonserializer)
|
||||
|
||||
|
||||
def test_deserialize_new_yaml_cassette():
|
||||
with open('tests/fixtures/migration/new_cassette.yaml', 'r') as f:
|
||||
deserialize(f.read(), yamlserializer)
|
||||
|
||||
|
||||
def test_deserialize_new_json_cassette():
|
||||
with open('tests/fixtures/migration/new_cassette.json', 'r') as f:
|
||||
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'))
|
||||
def test_serialize_constructs_UnicodeDecodeError(mock_dumps):
|
||||
with pytest.raises(UnicodeDecodeError):
|
||||
jsonserializer.serialize({})
|
||||
210
tests/unit/test_vcr.py
Normal file
210
tests/unit/test_vcr.py
Normal file
@@ -0,0 +1,210 @@
|
||||
import os
|
||||
|
||||
import pytest
|
||||
|
||||
from vcr import VCR, use_cassette
|
||||
from vcr.compat import mock
|
||||
from vcr.request import Request
|
||||
from vcr.stubs import VCRHTTPSConnection
|
||||
|
||||
|
||||
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:
|
||||
@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
|
||||
|
||||
# Make sure that calls to function now use cassettes with the
|
||||
# new filter_header_settings
|
||||
test_vcr.record_mode = mock.Mock()
|
||||
function()
|
||||
assert mock_cassette_load.call_args[1]['record_mode'] == test_vcr.record_mode
|
||||
|
||||
# Ensure that explicitly provided arguments still supercede
|
||||
# those on the vcr.
|
||||
new_record_mode = mock.Mock()
|
||||
|
||||
with test_vcr.use_cassette('test', record_mode=new_record_mode) as cassette:
|
||||
assert cassette.record_mode == new_record_mode
|
||||
|
||||
|
||||
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',))
|
||||
|
||||
with test_vcr.use_cassette('test') as cassette:
|
||||
assert cassette.filter_request(Request('GET', base_path + 'get', '', {})) is None
|
||||
assert cassette.filter_request(Request('GET', base_path + 'get2', '', {})) is not None
|
||||
|
||||
assert cassette.filter_request(Request('GET', base_path + '?foo=bar', '', {})).query == []
|
||||
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
|
||||
|
||||
with test_vcr.use_cassette('test', before_record_request=None) as cassette:
|
||||
# Test that before_record can be overwritten with
|
||||
assert cassette.filter_request(Request('GET', base_path + 'get', '', {})) is not None
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def random_fixture():
|
||||
return 1
|
||||
|
||||
|
||||
@use_cassette('test')
|
||||
def test_fixtures_with_use_cassette(random_fixture):
|
||||
# Applying a decorator to a test function that requests features can cause
|
||||
# problems if the decorator does not preserve the signature of the original
|
||||
# test function.
|
||||
|
||||
# This test ensures that use_cassette preserves the signature of
|
||||
# the original test function, and thus that use_cassette is
|
||||
# compatible with py.test fixtures. It is admittedly a bit strange
|
||||
# because the test would never even run if the relevant feature
|
||||
# were broken.
|
||||
pass
|
||||
|
||||
|
||||
def test_custom_patchers():
|
||||
class Test(object):
|
||||
attribute = None
|
||||
attribute2 = None
|
||||
test_vcr = VCR(custom_patches=((Test, 'attribute', VCRHTTPSConnection),))
|
||||
with test_vcr.use_cassette('custom_patches'):
|
||||
assert issubclass(Test.attribute, VCRHTTPSConnection)
|
||||
assert VCRHTTPSConnection is not Test.attribute
|
||||
|
||||
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
|
||||
|
||||
|
||||
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'
|
||||
|
||||
@vcr.use_cassette('test', record_mode='once', inject_cassette=False)
|
||||
def without_cassette_injected():
|
||||
pass
|
||||
|
||||
with_cassette_injected()
|
||||
without_cassette_injected()
|
||||
|
||||
|
||||
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)
|
||||
|
||||
def assert_record_mode_once(cassette):
|
||||
assert cassette.record_mode == 'once'
|
||||
|
||||
def assert_record_mode_all(cassette):
|
||||
assert cassette.record_mode == 'all'
|
||||
|
||||
changing_defaults(assert_record_mode_once)
|
||||
current_defaults(assert_record_mode_once)
|
||||
|
||||
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()
|
||||
141
tox.ini
141
tox.ini
@@ -1,127 +1,32 @@
|
||||
# Tox (http://tox.testrun.org/) is a tool for running tests
|
||||
# in multiple virtualenvs. This configuration file will run the
|
||||
# test suite on all supported python versions. To use it, "pip install tox"
|
||||
# and then run "tox" from this directory.
|
||||
|
||||
[tox]
|
||||
envlist =
|
||||
py26
|
||||
py27
|
||||
py33
|
||||
pypy
|
||||
py26requests
|
||||
py27requests
|
||||
py33requests
|
||||
pypyrequests
|
||||
py26oldrequests
|
||||
py27oldrequests
|
||||
py33oldrequests
|
||||
pypyoldrequests
|
||||
py26httplib2
|
||||
py27httplib2
|
||||
py33httplib2
|
||||
pypyhttplib2
|
||||
envlist = {py26,py27,py33,py34,pypy}-{requests27,requests26,requests25,requests24,requests23,requests22,requests1,httplib2,urllib317,urllib319,urllib3110,tornado,boto}
|
||||
|
||||
[testenv]
|
||||
commands =
|
||||
py.test {posargs}
|
||||
basepython =
|
||||
py26: python2.6
|
||||
py27: python2.7
|
||||
py33: python3.3
|
||||
py34: python3.4
|
||||
pypy: pypy
|
||||
deps =
|
||||
mock
|
||||
pytest
|
||||
pytest-localserver
|
||||
PyYAML
|
||||
|
||||
[testenv:py26oldrequests]
|
||||
basepython = python2.6
|
||||
deps =
|
||||
mock
|
||||
pytest
|
||||
PyYAML
|
||||
requests==1.2.3
|
||||
|
||||
[testenv:py27oldrequests]
|
||||
basepython = python2.7
|
||||
deps =
|
||||
mock
|
||||
pytest
|
||||
PyYAML
|
||||
requests==1.2.3
|
||||
|
||||
[testenv:py33oldrequests]
|
||||
basepython = python3.3
|
||||
deps =
|
||||
mock
|
||||
pytest
|
||||
PyYAML
|
||||
requests==1.2.3
|
||||
|
||||
[testenv:pypyoldrequests]
|
||||
basepython = pypy
|
||||
deps =
|
||||
mock
|
||||
pytest
|
||||
PyYAML
|
||||
requests==1.2.3
|
||||
|
||||
[testenv:py26requests]
|
||||
basepython = python2.6
|
||||
deps =
|
||||
mock
|
||||
pytest
|
||||
PyYAML
|
||||
requests
|
||||
|
||||
[testenv:py27requests]
|
||||
basepython = python2.7
|
||||
deps =
|
||||
mock
|
||||
pytest
|
||||
PyYAML
|
||||
requests
|
||||
|
||||
[testenv:py33requests]
|
||||
basepython = python3.3
|
||||
deps =
|
||||
mock
|
||||
pytest
|
||||
PyYAML
|
||||
requests
|
||||
|
||||
[testenv:pypyrequests]
|
||||
basepython = pypy
|
||||
deps =
|
||||
mock
|
||||
pytest
|
||||
PyYAML
|
||||
requests
|
||||
|
||||
[testenv:py26httplib2]
|
||||
basepython = python2.6
|
||||
deps =
|
||||
mock
|
||||
pytest
|
||||
PyYAML
|
||||
httplib2
|
||||
|
||||
[testenv:py27httplib2]
|
||||
basepython = python2.7
|
||||
deps =
|
||||
mock
|
||||
pytest
|
||||
PyYAML
|
||||
httplib2
|
||||
|
||||
[testenv:py33httplib2]
|
||||
basepython = python3.3
|
||||
deps =
|
||||
mock
|
||||
pytest
|
||||
PyYAML
|
||||
httplib2
|
||||
|
||||
[testenv:pypyhttplib2]
|
||||
basepython = pypy
|
||||
deps =
|
||||
mock
|
||||
pytest
|
||||
PyYAML
|
||||
httplib2
|
||||
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
|
||||
requests22: requests==2.2.1
|
||||
httplib2: httplib2
|
||||
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
|
||||
|
||||
@@ -1,7 +1,17 @@
|
||||
import logging
|
||||
from .config import VCR
|
||||
|
||||
# Set default logging handler to avoid "No handler found" warnings.
|
||||
try: # Python 2.7+
|
||||
from logging import NullHandler
|
||||
except ImportError:
|
||||
class NullHandler(logging.Handler):
|
||||
def emit(self, record):
|
||||
pass
|
||||
|
||||
|
||||
logging.getLogger(__name__).addHandler(NullHandler())
|
||||
|
||||
|
||||
default_vcr = VCR()
|
||||
|
||||
|
||||
def use_cassette(path, **kwargs):
|
||||
return default_vcr.use_cassette(path, **kwargs)
|
||||
use_cassette = default_vcr.use_cassette
|
||||
|
||||
245
vcr/cassette.py
245
vcr/cassette.py
@@ -1,51 +1,182 @@
|
||||
'''The container for recorded requests and responses'''
|
||||
import sys
|
||||
import inspect
|
||||
import logging
|
||||
|
||||
try:
|
||||
from collections import Counter, OrderedDict
|
||||
except ImportError:
|
||||
from .compat.counter import Counter
|
||||
from .compat.ordereddict import OrderedDict
|
||||
import wrapt
|
||||
|
||||
from contextdecorator import ContextDecorator
|
||||
|
||||
# Internal imports
|
||||
from .patch import install, reset
|
||||
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, url, method
|
||||
from .errors import UnhandledHTTPRequestError
|
||||
from .util import partition_dict
|
||||
|
||||
|
||||
class Cassette(ContextDecorator):
|
||||
'''A container for recorded requests and responses'''
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
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.
|
||||
"""
|
||||
|
||||
_non_cassette_arguments = ('path_transformer', 'func_path_generator')
|
||||
|
||||
@classmethod
|
||||
def load(cls, path, **kwargs):
|
||||
'''Load in the cassette stored at the provided path'''
|
||||
new_cassette = cls(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
|
||||
self._args_getter = args_getter
|
||||
self.__finish = None
|
||||
|
||||
def _patch_generator(self, cassette):
|
||||
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))
|
||||
yield cassette
|
||||
log.debug('Exiting context for cassette at {0}.'.format(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."
|
||||
other_kwargs, cassette_kwargs = partition_dict(
|
||||
lambda key, _: key in self._non_cassette_arguments,
|
||||
self._args_getter()
|
||||
)
|
||||
if 'path_transformer' in other_kwargs:
|
||||
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):
|
||||
next(self.__finish, None)
|
||||
self.__finish = None
|
||||
|
||||
@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:
|
||||
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:
|
||||
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 Cassette(object):
|
||||
"""A container for recorded requests and responses"""
|
||||
|
||||
@classmethod
|
||||
def load(cls, **kwargs):
|
||||
"""Instantiate and load the cassette stored at the specified path."""
|
||||
new_cassette = cls(**kwargs)
|
||||
new_cassette._load()
|
||||
return new_cassette
|
||||
|
||||
def __init__(self,
|
||||
path,
|
||||
serializer=yamlserializer,
|
||||
record_mode='once',
|
||||
match_on=[url, method]):
|
||||
@classmethod
|
||||
def use_arg_getter(cls, arg_getter):
|
||||
return CassetteContextDecorator(cls, arg_getter)
|
||||
|
||||
@classmethod
|
||||
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):
|
||||
|
||||
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.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
|
||||
self.record_mode = record_mode
|
||||
|
||||
@property
|
||||
def play_count(self):
|
||||
return sum(self.play_counts.values())
|
||||
|
||||
@property
|
||||
def all_played(self):
|
||||
"""Returns True if all responses have been played, False otherwise."""
|
||||
return self.play_count == len(self)
|
||||
|
||||
@property
|
||||
def requests(self):
|
||||
return [request for (request, response) in self.data]
|
||||
@@ -60,20 +191,43 @@ class Cassette(ContextDecorator):
|
||||
self.record_mode == 'none'
|
||||
|
||||
def append(self, request, response):
|
||||
'''Add a request, response pair to this cassette'''
|
||||
"""Add a request, response pair to this cassette"""
|
||||
request = self._before_record_request(request)
|
||||
if not request:
|
||||
return
|
||||
if self._before_record_response:
|
||||
response = self._before_record_response(response)
|
||||
self.data.append((request, response))
|
||||
self.dirty = True
|
||||
|
||||
def play_response(self, request):
|
||||
'''
|
||||
Get the response corresponding to a request, but only if it
|
||||
hasn't been played back before, and mark it as played
|
||||
'''
|
||||
def filter_request(self, request):
|
||||
return self._before_record_request(request)
|
||||
|
||||
def _responses(self, request):
|
||||
"""
|
||||
internal API, returns an iterator with all responses matching
|
||||
the request.
|
||||
"""
|
||||
request = self._before_record_request(request)
|
||||
for index, (stored_request, response) in enumerate(self.data):
|
||||
if requests_match(request, stored_request, self._match_on):
|
||||
if self.play_counts[index] == 0:
|
||||
self.play_counts[index] += 1
|
||||
return response
|
||||
yield index, response
|
||||
|
||||
def can_play_response_for(self, request):
|
||||
request = self._before_record_request(request)
|
||||
return request and request in self and \
|
||||
self.record_mode != 'all' and \
|
||||
self.rewound
|
||||
|
||||
def play_response(self, request):
|
||||
"""
|
||||
Get the response corresponding to a request, but only if it
|
||||
hasn't been played back before, and mark it as played
|
||||
"""
|
||||
for index, response in self._responses(request):
|
||||
if self.play_counts[index] == 0:
|
||||
self.play_counts[index] += 1
|
||||
return response
|
||||
# The cassette doesn't contain the request asked for.
|
||||
raise UnhandledHTTPRequestError(
|
||||
"The cassette (%r) doesn't contain the request (%r) asked for"
|
||||
@@ -81,14 +235,12 @@ class Cassette(ContextDecorator):
|
||||
)
|
||||
|
||||
def responses_of(self, request):
|
||||
'''
|
||||
"""
|
||||
Find the responses corresponding to a request.
|
||||
This function isn't actually used by VCR internally, but is
|
||||
provided as an external API.
|
||||
'''
|
||||
responses = \
|
||||
[resp for req, resp in self.data if
|
||||
requests_match(req, request, self._match_on)]
|
||||
"""
|
||||
responses = [response for index, response in self._responses(request)]
|
||||
|
||||
if responses:
|
||||
return responses
|
||||
@@ -129,21 +281,12 @@ class Cassette(ContextDecorator):
|
||||
)
|
||||
|
||||
def __len__(self):
|
||||
'''Return the number of request,response pairs stored in here'''
|
||||
"""Return the number of request,response pairs stored in here"""
|
||||
return len(self.data)
|
||||
|
||||
def __contains__(self, request):
|
||||
'''Return whether or not a request has been stored'''
|
||||
for stored_request, response in self.data:
|
||||
if requests_match(stored_request, request, self._match_on):
|
||||
"""Return whether or not a request has been stored"""
|
||||
for index, response in self._responses(request):
|
||||
if self.play_counts[index] == 0:
|
||||
return True
|
||||
return False
|
||||
|
||||
def __enter__(self):
|
||||
'''Patch the fetching libraries we know about'''
|
||||
install(self)
|
||||
return self
|
||||
|
||||
def __exit__(self, typ, value, traceback):
|
||||
self._save()
|
||||
reset()
|
||||
|
||||
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']
|
||||
@@ -1,193 +0,0 @@
|
||||
from operator import itemgetter
|
||||
from heapq import nlargest
|
||||
from itertools import repeat, ifilter
|
||||
|
||||
# From http://code.activestate.com/recipes/576611-counter-class/
|
||||
# Backported for python 2.6 support
|
||||
|
||||
class Counter(dict):
|
||||
'''Dict subclass for counting hashable objects. Sometimes called a bag
|
||||
or multiset. Elements are stored as dictionary keys and their counts
|
||||
are stored as dictionary values.
|
||||
|
||||
>>> Counter('zyzygy')
|
||||
Counter({'y': 3, 'z': 2, 'g': 1})
|
||||
|
||||
'''
|
||||
|
||||
def __init__(self, iterable=None, **kwds):
|
||||
'''Create a new, empty Counter object. And if given, count elements
|
||||
from an input iterable. Or, initialize the count from another mapping
|
||||
of elements to their counts.
|
||||
|
||||
>>> c = Counter() # a new, empty counter
|
||||
>>> c = Counter('gallahad') # a new counter from an iterable
|
||||
>>> c = Counter({'a': 4, 'b': 2}) # a new counter from a mapping
|
||||
>>> c = Counter(a=4, b=2) # a new counter from keyword args
|
||||
|
||||
'''
|
||||
self.update(iterable, **kwds)
|
||||
|
||||
def __missing__(self, key):
|
||||
return 0
|
||||
|
||||
def most_common(self, n=None):
|
||||
'''List the n most common elements and their counts from the most
|
||||
common to the least. If n is None, then list all element counts.
|
||||
|
||||
>>> Counter('abracadabra').most_common(3)
|
||||
[('a', 5), ('r', 2), ('b', 2)]
|
||||
|
||||
'''
|
||||
if n is None:
|
||||
return sorted(self.iteritems(), key=itemgetter(1), reverse=True)
|
||||
return nlargest(n, self.iteritems(), key=itemgetter(1))
|
||||
|
||||
def elements(self):
|
||||
'''Iterator over elements repeating each as many times as its count.
|
||||
|
||||
>>> c = Counter('ABCABC')
|
||||
>>> sorted(c.elements())
|
||||
['A', 'A', 'B', 'B', 'C', 'C']
|
||||
|
||||
If an element's count has been set to zero or is a negative number,
|
||||
elements() will ignore it.
|
||||
|
||||
'''
|
||||
for elem, count in self.iteritems():
|
||||
for _ in repeat(None, count):
|
||||
yield elem
|
||||
|
||||
# Override dict methods where the meaning changes for Counter objects.
|
||||
|
||||
@classmethod
|
||||
def fromkeys(cls, iterable, v=None):
|
||||
raise NotImplementedError(
|
||||
'Counter.fromkeys() is undefined. Use Counter(iterable) instead.')
|
||||
|
||||
def update(self, iterable=None, **kwds):
|
||||
'''Like dict.update() but add counts instead of replacing them.
|
||||
|
||||
Source can be an iterable, a dictionary, or another Counter instance.
|
||||
|
||||
>>> c = Counter('which')
|
||||
>>> c.update('witch') # add elements from another iterable
|
||||
>>> d = Counter('watch')
|
||||
>>> c.update(d) # add elements from another counter
|
||||
>>> c['h'] # four 'h' in which, witch, and watch
|
||||
4
|
||||
|
||||
'''
|
||||
if iterable is not None:
|
||||
if hasattr(iterable, 'iteritems'):
|
||||
if self:
|
||||
self_get = self.get
|
||||
for elem, count in iterable.iteritems():
|
||||
self[elem] = self_get(elem, 0) + count
|
||||
else:
|
||||
dict.update(self, iterable) # fast path when counter is empty
|
||||
else:
|
||||
self_get = self.get
|
||||
for elem in iterable:
|
||||
self[elem] = self_get(elem, 0) + 1
|
||||
if kwds:
|
||||
self.update(kwds)
|
||||
|
||||
def copy(self):
|
||||
'Like dict.copy() but returns a Counter instance instead of a dict.'
|
||||
return Counter(self)
|
||||
|
||||
def __delitem__(self, elem):
|
||||
'Like dict.__delitem__() but does not raise KeyError for missing values.'
|
||||
if elem in self:
|
||||
dict.__delitem__(self, elem)
|
||||
|
||||
def __repr__(self):
|
||||
if not self:
|
||||
return '%s()' % self.__class__.__name__
|
||||
items = ', '.join(map('%r: %r'.__mod__, self.most_common()))
|
||||
return '%s({%s})' % (self.__class__.__name__, items)
|
||||
|
||||
# Multiset-style mathematical operations discussed in:
|
||||
# Knuth TAOCP Volume II section 4.6.3 exercise 19
|
||||
# and at http://en.wikipedia.org/wiki/Multiset
|
||||
#
|
||||
# Outputs guaranteed to only include positive counts.
|
||||
#
|
||||
# To strip negative and zero counts, add-in an empty counter:
|
||||
# c += Counter()
|
||||
|
||||
def __add__(self, other):
|
||||
'''Add counts from two counters.
|
||||
|
||||
>>> Counter('abbb') + Counter('bcc')
|
||||
Counter({'b': 4, 'c': 2, 'a': 1})
|
||||
|
||||
|
||||
'''
|
||||
if not isinstance(other, Counter):
|
||||
return NotImplemented
|
||||
result = Counter()
|
||||
for elem in set(self) | set(other):
|
||||
newcount = self[elem] + other[elem]
|
||||
if newcount > 0:
|
||||
result[elem] = newcount
|
||||
return result
|
||||
|
||||
def __sub__(self, other):
|
||||
''' Subtract count, but keep only results with positive counts.
|
||||
|
||||
>>> Counter('abbbc') - Counter('bccd')
|
||||
Counter({'b': 2, 'a': 1})
|
||||
|
||||
'''
|
||||
if not isinstance(other, Counter):
|
||||
return NotImplemented
|
||||
result = Counter()
|
||||
for elem in set(self) | set(other):
|
||||
newcount = self[elem] - other[elem]
|
||||
if newcount > 0:
|
||||
result[elem] = newcount
|
||||
return result
|
||||
|
||||
def __or__(self, other):
|
||||
'''Union is the maximum of value in either of the input counters.
|
||||
|
||||
>>> Counter('abbb') | Counter('bcc')
|
||||
Counter({'b': 3, 'c': 2, 'a': 1})
|
||||
|
||||
'''
|
||||
if not isinstance(other, Counter):
|
||||
return NotImplemented
|
||||
_max = max
|
||||
result = Counter()
|
||||
for elem in set(self) | set(other):
|
||||
newcount = _max(self[elem], other[elem])
|
||||
if newcount > 0:
|
||||
result[elem] = newcount
|
||||
return result
|
||||
|
||||
def __and__(self, other):
|
||||
''' Intersection is the minimum of corresponding counts.
|
||||
|
||||
>>> Counter('abbb') & Counter('bcc')
|
||||
Counter({'b': 1})
|
||||
|
||||
'''
|
||||
if not isinstance(other, Counter):
|
||||
return NotImplemented
|
||||
_min = min
|
||||
result = Counter()
|
||||
if len(self) < len(other):
|
||||
self, other = other, self
|
||||
for elem in ifilter(self.__contains__, other):
|
||||
newcount = _min(self[elem], other[elem])
|
||||
if newcount > 0:
|
||||
result[elem] = newcount
|
||||
return result
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
import doctest
|
||||
print doctest.testmod()
|
||||
|
||||
@@ -1,258 +0,0 @@
|
||||
# Backport of OrderedDict() class that runs on Python 2.4, 2.5, 2.6, 2.7 and pypy.
|
||||
# Passes Python2.7's test suite and incorporates all the latest updates.
|
||||
|
||||
try:
|
||||
from thread import get_ident as _get_ident
|
||||
except ImportError:
|
||||
from dummy_thread import get_ident as _get_ident
|
||||
|
||||
try:
|
||||
from _abcoll import KeysView, ValuesView, ItemsView
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
|
||||
class OrderedDict(dict):
|
||||
'Dictionary that remembers insertion order'
|
||||
# An inherited dict maps keys to values.
|
||||
# The inherited dict provides __getitem__, __len__, __contains__, and get.
|
||||
# The remaining methods are order-aware.
|
||||
# Big-O running times for all methods are the same as for regular dictionaries.
|
||||
|
||||
# The internal self.__map dictionary maps keys to links in a doubly linked list.
|
||||
# The circular doubly linked list starts and ends with a sentinel element.
|
||||
# The sentinel element never gets deleted (this simplifies the algorithm).
|
||||
# Each link is stored as a list of length three: [PREV, NEXT, KEY].
|
||||
|
||||
def __init__(self, *args, **kwds):
|
||||
'''Initialize an ordered dictionary. Signature is the same as for
|
||||
regular dictionaries, but keyword arguments are not recommended
|
||||
because their insertion order is arbitrary.
|
||||
|
||||
'''
|
||||
if len(args) > 1:
|
||||
raise TypeError('expected at most 1 arguments, got %d' % len(args))
|
||||
try:
|
||||
self.__root
|
||||
except AttributeError:
|
||||
self.__root = root = [] # sentinel node
|
||||
root[:] = [root, root, None]
|
||||
self.__map = {}
|
||||
self.__update(*args, **kwds)
|
||||
|
||||
def __setitem__(self, key, value, dict_setitem=dict.__setitem__):
|
||||
'od.__setitem__(i, y) <==> od[i]=y'
|
||||
# Setting a new item creates a new link which goes at the end of the linked
|
||||
# list, and the inherited dictionary is updated with the new key/value pair.
|
||||
if key not in self:
|
||||
root = self.__root
|
||||
last = root[0]
|
||||
last[1] = root[0] = self.__map[key] = [last, root, key]
|
||||
dict_setitem(self, key, value)
|
||||
|
||||
def __delitem__(self, key, dict_delitem=dict.__delitem__):
|
||||
'od.__delitem__(y) <==> del od[y]'
|
||||
# Deleting an existing item uses self.__map to find the link which is
|
||||
# then removed by updating the links in the predecessor and successor nodes.
|
||||
dict_delitem(self, key)
|
||||
link_prev, link_next, key = self.__map.pop(key)
|
||||
link_prev[1] = link_next
|
||||
link_next[0] = link_prev
|
||||
|
||||
def __iter__(self):
|
||||
'od.__iter__() <==> iter(od)'
|
||||
root = self.__root
|
||||
curr = root[1]
|
||||
while curr is not root:
|
||||
yield curr[2]
|
||||
curr = curr[1]
|
||||
|
||||
def __reversed__(self):
|
||||
'od.__reversed__() <==> reversed(od)'
|
||||
root = self.__root
|
||||
curr = root[0]
|
||||
while curr is not root:
|
||||
yield curr[2]
|
||||
curr = curr[0]
|
||||
|
||||
def clear(self):
|
||||
'od.clear() -> None. Remove all items from od.'
|
||||
try:
|
||||
for node in self.__map.itervalues():
|
||||
del node[:]
|
||||
root = self.__root
|
||||
root[:] = [root, root, None]
|
||||
self.__map.clear()
|
||||
except AttributeError:
|
||||
pass
|
||||
dict.clear(self)
|
||||
|
||||
def popitem(self, last=True):
|
||||
'''od.popitem() -> (k, v), return and remove a (key, value) pair.
|
||||
Pairs are returned in LIFO order if last is true or FIFO order if false.
|
||||
|
||||
'''
|
||||
if not self:
|
||||
raise KeyError('dictionary is empty')
|
||||
root = self.__root
|
||||
if last:
|
||||
link = root[0]
|
||||
link_prev = link[0]
|
||||
link_prev[1] = root
|
||||
root[0] = link_prev
|
||||
else:
|
||||
link = root[1]
|
||||
link_next = link[1]
|
||||
root[1] = link_next
|
||||
link_next[0] = root
|
||||
key = link[2]
|
||||
del self.__map[key]
|
||||
value = dict.pop(self, key)
|
||||
return key, value
|
||||
|
||||
# -- the following methods do not depend on the internal structure --
|
||||
|
||||
def keys(self):
|
||||
'od.keys() -> list of keys in od'
|
||||
return list(self)
|
||||
|
||||
def values(self):
|
||||
'od.values() -> list of values in od'
|
||||
return [self[key] for key in self]
|
||||
|
||||
def items(self):
|
||||
'od.items() -> list of (key, value) pairs in od'
|
||||
return [(key, self[key]) for key in self]
|
||||
|
||||
def iterkeys(self):
|
||||
'od.iterkeys() -> an iterator over the keys in od'
|
||||
return iter(self)
|
||||
|
||||
def itervalues(self):
|
||||
'od.itervalues -> an iterator over the values in od'
|
||||
for k in self:
|
||||
yield self[k]
|
||||
|
||||
def iteritems(self):
|
||||
'od.iteritems -> an iterator over the (key, value) items in od'
|
||||
for k in self:
|
||||
yield (k, self[k])
|
||||
|
||||
def update(*args, **kwds):
|
||||
'''od.update(E, **F) -> None. Update od from dict/iterable E and F.
|
||||
|
||||
If E is a dict instance, does: for k in E: od[k] = E[k]
|
||||
If E has a .keys() method, does: for k in E.keys(): od[k] = E[k]
|
||||
Or if E is an iterable of items, does: for k, v in E: od[k] = v
|
||||
In either case, this is followed by: for k, v in F.items(): od[k] = v
|
||||
|
||||
'''
|
||||
if len(args) > 2:
|
||||
raise TypeError('update() takes at most 2 positional '
|
||||
'arguments (%d given)' % (len(args),))
|
||||
elif not args:
|
||||
raise TypeError('update() takes at least 1 argument (0 given)')
|
||||
self = args[0]
|
||||
# Make progressively weaker assumptions about "other"
|
||||
other = ()
|
||||
if len(args) == 2:
|
||||
other = args[1]
|
||||
if isinstance(other, dict):
|
||||
for key in other:
|
||||
self[key] = other[key]
|
||||
elif hasattr(other, 'keys'):
|
||||
for key in other.keys():
|
||||
self[key] = other[key]
|
||||
else:
|
||||
for key, value in other:
|
||||
self[key] = value
|
||||
for key, value in kwds.items():
|
||||
self[key] = value
|
||||
|
||||
__update = update # let subclasses override update without breaking __init__
|
||||
|
||||
__marker = object()
|
||||
|
||||
def pop(self, key, default=__marker):
|
||||
'''od.pop(k[,d]) -> v, remove specified key and return the corresponding value.
|
||||
If key is not found, d is returned if given, otherwise KeyError is raised.
|
||||
|
||||
'''
|
||||
if key in self:
|
||||
result = self[key]
|
||||
del self[key]
|
||||
return result
|
||||
if default is self.__marker:
|
||||
raise KeyError(key)
|
||||
return default
|
||||
|
||||
def setdefault(self, key, default=None):
|
||||
'od.setdefault(k[,d]) -> od.get(k,d), also set od[k]=d if k not in od'
|
||||
if key in self:
|
||||
return self[key]
|
||||
self[key] = default
|
||||
return default
|
||||
|
||||
def __repr__(self, _repr_running={}):
|
||||
'od.__repr__() <==> repr(od)'
|
||||
call_key = id(self), _get_ident()
|
||||
if call_key in _repr_running:
|
||||
return '...'
|
||||
_repr_running[call_key] = 1
|
||||
try:
|
||||
if not self:
|
||||
return '%s()' % (self.__class__.__name__,)
|
||||
return '%s(%r)' % (self.__class__.__name__, self.items())
|
||||
finally:
|
||||
del _repr_running[call_key]
|
||||
|
||||
def __reduce__(self):
|
||||
'Return state information for pickling'
|
||||
items = [[k, self[k]] for k in self]
|
||||
inst_dict = vars(self).copy()
|
||||
for k in vars(OrderedDict()):
|
||||
inst_dict.pop(k, None)
|
||||
if inst_dict:
|
||||
return (self.__class__, (items,), inst_dict)
|
||||
return self.__class__, (items,)
|
||||
|
||||
def copy(self):
|
||||
'od.copy() -> a shallow copy of od'
|
||||
return self.__class__(self)
|
||||
|
||||
@classmethod
|
||||
def fromkeys(cls, iterable, value=None):
|
||||
'''OD.fromkeys(S[, v]) -> New ordered dictionary with keys from S
|
||||
and values equal to v (which defaults to None).
|
||||
|
||||
'''
|
||||
d = cls()
|
||||
for key in iterable:
|
||||
d[key] = value
|
||||
return d
|
||||
|
||||
def __eq__(self, other):
|
||||
'''od.__eq__(y) <==> od==y. Comparison to another OD is order-sensitive
|
||||
while comparison to a regular mapping is order-insensitive.
|
||||
|
||||
'''
|
||||
if isinstance(other, OrderedDict):
|
||||
return len(self)==len(other) and self.items() == other.items()
|
||||
return dict.__eq__(self, other)
|
||||
|
||||
def __ne__(self, other):
|
||||
return not self == other
|
||||
|
||||
# -- the following methods are only used in Python 2.7 --
|
||||
|
||||
def viewkeys(self):
|
||||
"od.viewkeys() -> a set-like object providing a view on od's keys"
|
||||
return KeysView(self)
|
||||
|
||||
def viewvalues(self):
|
||||
"od.viewvalues() -> an object providing a view on od's values"
|
||||
return ValuesView(self)
|
||||
|
||||
def viewitems(self):
|
||||
"od.viewitems() -> a set-like object providing a view on od's items"
|
||||
return ItemsView(self)
|
||||
207
vcr/config.py
207
vcr/config.py
@@ -1,16 +1,35 @@
|
||||
import copy
|
||||
import functools
|
||||
import inspect
|
||||
import os
|
||||
|
||||
import six
|
||||
|
||||
from .compat import collections
|
||||
from .cassette import Cassette
|
||||
from .serializers import yamlserializer, jsonserializer
|
||||
from .matchers import method, url, host, path, headers, body
|
||||
from .util import compose
|
||||
from . import matchers
|
||||
from . import filters
|
||||
|
||||
|
||||
class VCR(object):
|
||||
def __init__(self,
|
||||
serializer='yaml',
|
||||
cassette_library_dir=None,
|
||||
record_mode="once",
|
||||
match_on=['url', 'method'],
|
||||
):
|
||||
|
||||
@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=lambda x: x, 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, 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
|
||||
@@ -19,14 +38,30 @@ class VCR(object):
|
||||
'json': jsonserializer,
|
||||
}
|
||||
self.matchers = {
|
||||
'method': method,
|
||||
'url': url,
|
||||
'host': host,
|
||||
'path': path,
|
||||
'headers': headers,
|
||||
'body': body,
|
||||
'method': matchers.method,
|
||||
'uri': matchers.uri,
|
||||
'url': matchers.uri, # matcher for backwards compatibility
|
||||
'scheme': matchers.scheme,
|
||||
'host': matchers.host,
|
||||
'port': matchers.port,
|
||||
'path': matchers.path,
|
||||
'query': matchers.query,
|
||||
'headers': matchers.headers,
|
||||
'raw_body': matchers.raw_body,
|
||||
'body': matchers.body,
|
||||
}
|
||||
self.record_mode = record_mode
|
||||
self.filter_headers = filter_headers
|
||||
self.filter_query_parameters = filter_query_parameters
|
||||
self.filter_post_data_parameters = filter_post_data_parameters
|
||||
self.before_record_request = before_record_request or before_record
|
||||
self.before_record_response = before_record_response
|
||||
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:
|
||||
@@ -45,29 +80,157 @@ class VCR(object):
|
||||
matchers.append(self.matchers[m])
|
||||
except KeyError:
|
||||
raise KeyError(
|
||||
"Matcher {0} doesn't exist or isn't registered".format(
|
||||
m)
|
||||
"Matcher {0} doesn't exist or isn't registered".format(m)
|
||||
)
|
||||
return matchers
|
||||
|
||||
def use_cassette(self, path, **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:
|
||||
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_merged_config, **kwargs)
|
||||
return Cassette.use_arg_getter(args_getter)
|
||||
|
||||
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 or self.ensure_suffix('.yaml')
|
||||
)
|
||||
func_path_generator = kwargs.get(
|
||||
'func_path_generator',
|
||||
self.func_path_generator
|
||||
)
|
||||
cassette_library_dir = kwargs.get(
|
||||
'cassette_library_dir',
|
||||
self.cassette_library_dir
|
||||
)
|
||||
|
||||
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),
|
||||
"record_mode": kwargs.get('record_mode', self.record_mode),
|
||||
'serializer': self._get_serializer(serializer_name),
|
||||
'match_on': self._get_matchers(matcher_names),
|
||||
'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
|
||||
),
|
||||
'custom_patches': self._custom_patches + kwargs.get(
|
||||
'custom_patches', ()
|
||||
),
|
||||
'inject': kwargs.get('inject_cassette', self.inject_cassette),
|
||||
'path_transformer': path_transformer,
|
||||
'func_path_generator': func_path_generator
|
||||
}
|
||||
path = kwargs.get('path')
|
||||
if path:
|
||||
merged_config['path'] = path
|
||||
return merged_config
|
||||
|
||||
return Cassette.load(path, **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)
|
||||
def before_record_response(response):
|
||||
for function in filter_functions:
|
||||
if response is None:
|
||||
break
|
||||
response = function(response)
|
||||
return response
|
||||
return before_record_response
|
||||
|
||||
def _build_before_record_request(self, options):
|
||||
filter_functions = []
|
||||
filter_headers = options.get(
|
||||
'filter_headers', self.filter_headers
|
||||
)
|
||||
filter_query_parameters = options.get(
|
||||
'filter_query_parameters', self.filter_query_parameters
|
||||
)
|
||||
filter_post_data_parameters = options.get(
|
||||
'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)
|
||||
)
|
||||
ignore_hosts = options.get(
|
||||
'ignore_hosts', self.ignore_hosts
|
||||
)
|
||||
ignore_localhost = options.get(
|
||||
'ignore_localhost', self.ignore_localhost
|
||||
)
|
||||
if 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))
|
||||
if 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)
|
||||
if ignore_localhost:
|
||||
hosts_to_ignore.extend(('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)
|
||||
def before_record_request(request):
|
||||
request = copy.copy(request)
|
||||
for function in filter_functions:
|
||||
if request is None:
|
||||
break
|
||||
request = function(request)
|
||||
return request
|
||||
|
||||
return before_record_request
|
||||
|
||||
@staticmethod
|
||||
def _build_ignore_hosts(hosts_to_ignore):
|
||||
def filter_ignored_hosts(request):
|
||||
if hasattr(request, 'host') and request.host in hosts_to_ignore:
|
||||
return
|
||||
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
|
||||
|
||||
53
vcr/filters.py
Normal file
53
vcr/filters.py
Normal file
@@ -0,0 +1,53 @@
|
||||
from six import BytesIO, text_type
|
||||
from six.moves.urllib.parse import urlparse, urlencode, urlunparse
|
||||
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
|
||||
return request
|
||||
|
||||
|
||||
def remove_query_parameters(request, query_parameters_to_remove):
|
||||
query = request.query
|
||||
new_query = [(k, v) for (k, v) in query
|
||||
if k not in query_parameters_to_remove]
|
||||
if len(new_query) != len(query):
|
||||
uri_parts = list(urlparse(request.uri))
|
||||
uri_parts[4] = urlencode(new_query)
|
||||
request.uri = urlunparse(uri_parts)
|
||||
return request
|
||||
|
||||
|
||||
def remove_post_data_parameters(request, post_data_parameters_to_remove):
|
||||
if request.method == 'POST' and not isinstance(request.body, BytesIO):
|
||||
if ('Content-Type' in request.headers and
|
||||
request.headers['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,26 +1,98 @@
|
||||
import json
|
||||
from six.moves import urllib, xmlrpc_client
|
||||
from .util import CaseInsensitiveDict, read_body
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def method(r1, r2):
|
||||
return r1.method == r2.method
|
||||
|
||||
|
||||
def url(r1, r2):
|
||||
return r1.url == r2.url
|
||||
def uri(r1, r2):
|
||||
return r1.uri == r2.uri
|
||||
|
||||
|
||||
def host(r1, r2):
|
||||
return r1.host == r2.host
|
||||
|
||||
|
||||
def scheme(r1, r2):
|
||||
return r1.scheme == r2.scheme
|
||||
|
||||
|
||||
def port(r1, r2):
|
||||
return r1.port == r2.port
|
||||
|
||||
|
||||
def path(r1, r2):
|
||||
return r1.path == r2.path
|
||||
|
||||
|
||||
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):
|
||||
headers = CaseInsensitiveDict(request.headers)
|
||||
for checker, transformer in _checker_transformer_pairs:
|
||||
if checker(headers): return transformer
|
||||
else:
|
||||
return _identity
|
||||
|
||||
|
||||
def body(r1, r2):
|
||||
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):
|
||||
return r1.headers == r2.headers
|
||||
|
||||
|
||||
def _log_matches(r1, r2, matches):
|
||||
differences = [m for m in matches if not m[0]]
|
||||
if differences:
|
||||
log.debug(
|
||||
"Requests {0} and {1} differ according to "
|
||||
"the following matchers: {2}".format(r1, r2, differences)
|
||||
)
|
||||
|
||||
|
||||
def requests_match(r1, r2, matchers):
|
||||
return all(m(r1, r2) for m in matchers)
|
||||
matches = [(m(r1, r2), m) for m in matchers]
|
||||
_log_matches(r1, r2, matches)
|
||||
return all([m[0] for m in matches])
|
||||
|
||||
168
vcr/migration.py
Normal file
168
vcr/migration.py
Normal file
@@ -0,0 +1,168 @@
|
||||
"""
|
||||
Migration script for old 'yaml' and 'json' cassettes
|
||||
|
||||
.. warning:: Backup your cassettes files before migration.
|
||||
|
||||
It merges and deletes the request obsolete keys (protocol, host, port, path)
|
||||
into new 'uri' key.
|
||||
Usage::
|
||||
|
||||
python -m vcr.migration PATH
|
||||
|
||||
The PATH can be path to the directory with cassettes or cassette itself
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import sys
|
||||
import tempfile
|
||||
import yaml
|
||||
|
||||
from .serializers import compat, yamlserializer, jsonserializer
|
||||
from .serialize import serialize
|
||||
from . import request
|
||||
from .stubs.compat import get_httpmessage
|
||||
|
||||
# Use the libYAML versions if possible
|
||||
try:
|
||||
from yaml import CLoader as Loader
|
||||
except ImportError:
|
||||
from yaml import Loader
|
||||
|
||||
|
||||
def preprocess_yaml(cassette):
|
||||
# this is the hack that makes the whole thing work. The old version used
|
||||
# to deserialize to Request objects automatically using pyYaml's !!python
|
||||
# tag system. This made it difficult to deserialize old cassettes on new
|
||||
# versions. So this just strips the tags before deserializing.
|
||||
|
||||
STRINGS_TO_NUKE = [
|
||||
'!!python/object:vcr.request.Request',
|
||||
'!!python/object/apply:__builtin__.frozenset',
|
||||
'!!python/object/apply:builtins.frozenset',
|
||||
]
|
||||
for s in STRINGS_TO_NUKE:
|
||||
cassette = cassette.replace(s, '')
|
||||
return cassette
|
||||
|
||||
|
||||
PARTS = [
|
||||
'protocol',
|
||||
'host',
|
||||
'port',
|
||||
'path',
|
||||
]
|
||||
|
||||
|
||||
def build_uri(**parts):
|
||||
port = parts['port']
|
||||
scheme = parts['protocol']
|
||||
default_port = {'https': 443, 'http': 80}[scheme]
|
||||
parts['port'] = ':{0}'.format(port) if port != default_port else ''
|
||||
return "{protocol}://{host}{port}{path}".format(**parts)
|
||||
|
||||
|
||||
def _migrate(data):
|
||||
interactions = []
|
||||
for item in data:
|
||||
req = item['request']
|
||||
res = item['response']
|
||||
uri = dict((k, req.pop(k)) for k in PARTS)
|
||||
req['uri'] = build_uri(**uri)
|
||||
# convert headers to dict of lists
|
||||
headers = req['headers']
|
||||
for k in headers:
|
||||
headers[k] = [headers[k]]
|
||||
response_headers = {}
|
||||
for k, v in get_httpmessage(
|
||||
b"".join(h.encode('utf-8') for h in res['headers'])
|
||||
).items():
|
||||
response_headers.setdefault(k, [])
|
||||
response_headers[k].append(v)
|
||||
res['headers'] = response_headers
|
||||
interactions.append({'request': req, 'response': res})
|
||||
return {
|
||||
'requests': [
|
||||
request.Request._from_dict(i['request']) for i in interactions
|
||||
],
|
||||
'responses': [i['response'] for i in interactions],
|
||||
}
|
||||
|
||||
|
||||
def migrate_json(in_fp, out_fp):
|
||||
data = json.load(in_fp)
|
||||
if _already_migrated(data):
|
||||
return False
|
||||
interactions = _migrate(data)
|
||||
out_fp.write(serialize(interactions, jsonserializer))
|
||||
return True
|
||||
|
||||
|
||||
def _list_of_tuples_to_dict(fs):
|
||||
return dict((k, v) for k, v in fs[0])
|
||||
|
||||
|
||||
def _already_migrated(data):
|
||||
try:
|
||||
if data.get('version') == 1:
|
||||
return True
|
||||
except AttributeError:
|
||||
return False
|
||||
|
||||
|
||||
def migrate_yml(in_fp, out_fp):
|
||||
data = yaml.load(preprocess_yaml(in_fp.read()), Loader=Loader)
|
||||
if _already_migrated(data):
|
||||
return False
|
||||
for i in range(len(data)):
|
||||
data[i]['request']['headers'] = _list_of_tuples_to_dict(
|
||||
data[i]['request']['headers']
|
||||
)
|
||||
interactions = _migrate(data)
|
||||
out_fp.write(serialize(interactions, yamlserializer))
|
||||
return True
|
||||
|
||||
|
||||
def migrate(file_path, migration_fn):
|
||||
# because we assume that original files can be reverted
|
||||
# we will try to copy the content. (os.rename not needed)
|
||||
with tempfile.TemporaryFile(mode='w+') as out_fp:
|
||||
with open(file_path, 'r') as in_fp:
|
||||
if not migration_fn(in_fp, out_fp):
|
||||
return False
|
||||
with open(file_path, 'w') as in_fp:
|
||||
out_fp.seek(0)
|
||||
shutil.copyfileobj(out_fp, in_fp)
|
||||
return True
|
||||
|
||||
|
||||
def try_migrate(path):
|
||||
if path.endswith('.json'):
|
||||
return migrate(path, migrate_json)
|
||||
elif path.endswith('.yaml') or path.endswith('.yml'):
|
||||
return migrate(path, migrate_yml)
|
||||
return False
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) != 2:
|
||||
raise SystemExit("Please provide path to cassettes directory or file. "
|
||||
"Usage: python -m vcr.migration PATH")
|
||||
|
||||
path = sys.argv[1]
|
||||
if not os.path.isabs(path):
|
||||
path = os.path.abspath(path)
|
||||
files = [path]
|
||||
if os.path.isdir(path):
|
||||
files = (os.path.join(root, name)
|
||||
for (root, dirs, files) in os.walk(path)
|
||||
for name in files)
|
||||
for file_path in files:
|
||||
migrated = try_migrate(file_path)
|
||||
status = 'OK' if migrated else 'FAIL'
|
||||
sys.stderr.write("[{0}] {1}\n".format(status, file_path))
|
||||
sys.stderr.write("Done.\n")
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
413
vcr/patch.py
413
vcr/patch.py
@@ -1,5 +1,8 @@
|
||||
'''Utilities for patching in cassettes'''
|
||||
import functools
|
||||
import itertools
|
||||
|
||||
from .compat import contextlib, mock
|
||||
from .stubs import VCRHTTPConnection, VCRHTTPSConnection
|
||||
from six.moves import http_client as httplib
|
||||
|
||||
@@ -8,115 +11,375 @@ from six.moves import http_client as httplib
|
||||
_HTTPConnection = httplib.HTTPConnection
|
||||
_HTTPSConnection = httplib.HTTPSConnection
|
||||
|
||||
|
||||
# Try to save the original types for requests
|
||||
try:
|
||||
# Try to save the original types for requests
|
||||
import requests.packages.urllib3.connectionpool as cpool
|
||||
except ImportError: # pragma: no cover
|
||||
pass
|
||||
else:
|
||||
_VerifiedHTTPSConnection = cpool.VerifiedHTTPSConnection
|
||||
_cpoolHTTPConnection = cpool.HTTPConnection
|
||||
_cpoolHTTPSConnection = cpool.HTTPSConnection
|
||||
except ImportError: # pragma: no cover
|
||||
pass
|
||||
|
||||
|
||||
# Try to save the original types for urllib3
|
||||
try:
|
||||
# Try to save the original types for urllib3
|
||||
import urllib3
|
||||
_VerifiedHTTPSConnection = urllib3.connectionpool.VerifiedHTTPSConnection
|
||||
except ImportError: # pragma: no cover
|
||||
pass
|
||||
else:
|
||||
_VerifiedHTTPSConnection = urllib3.connectionpool.VerifiedHTTPSConnection
|
||||
|
||||
|
||||
# Try to save the original types for httplib2
|
||||
try:
|
||||
# Try to save the original types for httplib2
|
||||
import httplib2
|
||||
except ImportError: # pragma: no cover
|
||||
pass
|
||||
else:
|
||||
_HTTPConnectionWithTimeout = httplib2.HTTPConnectionWithTimeout
|
||||
_HTTPSConnectionWithTimeout = httplib2.HTTPSConnectionWithTimeout
|
||||
_SCHEME_TO_CONNECTION = httplib2.SCHEME_TO_CONNECTION
|
||||
|
||||
|
||||
# Try to save the original types for boto
|
||||
try:
|
||||
import boto.https_connection
|
||||
except ImportError: # pragma: no cover
|
||||
pass
|
||||
else:
|
||||
_CertValidatingHTTPSConnection = boto.https_connection.CertValidatingHTTPSConnection
|
||||
|
||||
|
||||
def install(cassette):
|
||||
"""
|
||||
Patch all the HTTPConnections references we can find!
|
||||
This replaces the actual HTTPConnection with a VCRHTTPConnection
|
||||
object which knows how to save to / read from cassettes
|
||||
"""
|
||||
httplib.HTTPConnection = VCRHTTPConnection
|
||||
httplib.HTTPSConnection = VCRHTTPSConnection
|
||||
httplib.HTTPConnection.cassette = cassette
|
||||
httplib.HTTPSConnection.cassette = cassette
|
||||
# Try to save the original types for Tornado
|
||||
try:
|
||||
import tornado.httpclient
|
||||
import tornado.simple_httpclient
|
||||
except ImportError: # pragma: no cover
|
||||
pass
|
||||
else:
|
||||
_AsyncHTTPClient = tornado.httpclient.AsyncHTTPClient
|
||||
_SimpleAsyncHTTPClient = tornado.simple_httpclient.SimpleAsyncHTTPClient
|
||||
|
||||
# patch requests v1.x
|
||||
|
||||
try:
|
||||
import tornado.curl_httpclient
|
||||
except ImportError: # pragma: no cover
|
||||
pass
|
||||
else:
|
||||
_CurlAsyncHTTPClient = tornado.curl_httpclient.CurlAsyncHTTPClient
|
||||
|
||||
|
||||
class CassettePatcherBuilder(object):
|
||||
|
||||
def _build_patchers_from_mock_triples_decorator(function):
|
||||
@functools.wraps(function)
|
||||
def wrapped(self, *args, **kwargs):
|
||||
return self._build_patchers_from_mock_triples(
|
||||
function(self, *args, **kwargs)
|
||||
)
|
||||
return wrapped
|
||||
|
||||
def __init__(self, cassette):
|
||||
self._cassette = cassette
|
||||
self._class_to_cassette_subclass = {}
|
||||
|
||||
def build(self):
|
||||
return itertools.chain(
|
||||
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):
|
||||
for args in mock_triples:
|
||||
patcher = self._build_patcher(*args)
|
||||
if patcher:
|
||||
yield patcher
|
||||
|
||||
def _build_patcher(self, obj, patched_attribute, replacement_class):
|
||||
if not hasattr(obj, patched_attribute):
|
||||
return
|
||||
|
||||
return mock.patch.object(obj, patched_attribute,
|
||||
self._recursively_apply_get_cassette_subclass(
|
||||
replacement_class))
|
||||
|
||||
def _recursively_apply_get_cassette_subclass(self, replacement_dict_or_obj):
|
||||
"""One of the subtleties of this class is that it does not directly
|
||||
replace HTTPSConnection with `VCRRequestsHTTPSConnection`, but a
|
||||
subclass of the aforementioned class that has the `cassette`
|
||||
class attribute assigned to `self._cassette`. This behavior is
|
||||
necessary to properly support nested cassette contexts.
|
||||
|
||||
This function exists to ensure that we use the same class
|
||||
object (reference) to patch everything that replaces
|
||||
VCRRequestHTTP[S]Connection, but that we can talk about
|
||||
patching them with the raw references instead, and without
|
||||
worrying about exactly where the subclass with the relevant
|
||||
value for `cassette` is first created.
|
||||
|
||||
The function is recursive because it looks in to dictionaries
|
||||
and replaces class values at any depth with the subclass
|
||||
described in the previous paragraph.
|
||||
"""
|
||||
if isinstance(replacement_dict_or_obj, dict):
|
||||
for key, replacement_obj in replacement_dict_or_obj.items():
|
||||
replacement_obj = self._recursively_apply_get_cassette_subclass(
|
||||
replacement_obj)
|
||||
replacement_dict_or_obj[key] = replacement_obj
|
||||
return replacement_dict_or_obj
|
||||
if hasattr(replacement_dict_or_obj, 'cassette'):
|
||||
replacement_dict_or_obj = self._get_cassette_subclass(
|
||||
replacement_dict_or_obj)
|
||||
return replacement_dict_or_obj
|
||||
|
||||
def _get_cassette_subclass(self, klass):
|
||||
if klass.cassette is not None:
|
||||
return klass
|
||||
if klass not in self._class_to_cassette_subclass:
|
||||
subclass = self._build_cassette_subclass(klass)
|
||||
self._class_to_cassette_subclass[klass] = subclass
|
||||
return self._class_to_cassette_subclass[klass]
|
||||
|
||||
def _build_cassette_subclass(self, base_class):
|
||||
bases = (base_class,)
|
||||
if not issubclass(base_class, object): # Check for old style class
|
||||
bases += (object,)
|
||||
return type('{0}{1}'.format(base_class.__name__, self._cassette._path),
|
||||
bases, dict(cassette=self._cassette))
|
||||
|
||||
@_build_patchers_from_mock_triples_decorator
|
||||
def _httplib(self):
|
||||
yield httplib, 'HTTPConnection', VCRHTTPConnection
|
||||
yield httplib, 'HTTPSConnection', VCRHTTPSConnection
|
||||
|
||||
def _requests(self):
|
||||
try:
|
||||
import requests.packages.urllib3.connectionpool as cpool
|
||||
except ImportError: # pragma: no cover
|
||||
return ()
|
||||
from .stubs import requests_stubs
|
||||
return self._urllib3_patchers(cpool, requests_stubs)
|
||||
|
||||
def _patched_get_conn(self, connection_pool_class, connection_class_getter):
|
||||
get_conn = connection_pool_class._get_conn
|
||||
@functools.wraps(get_conn)
|
||||
def patched_get_conn(pool, timeout=None):
|
||||
connection = get_conn(pool, timeout)
|
||||
connection_class = pool.ConnectionCls if hasattr(pool, 'ConnectionCls') \
|
||||
else connection_class_getter()
|
||||
# We need to make sure that we are actually providing a
|
||||
# patched version of the connection class. This might not
|
||||
# always be the case because the pool keeps previously
|
||||
# used connections (which might actually be of a different
|
||||
# class) around. This while loop will terminate because
|
||||
# eventually the pool will run out of connections.
|
||||
while not isinstance(connection, connection_class):
|
||||
connection = get_conn(pool, timeout)
|
||||
return connection
|
||||
return patched_get_conn
|
||||
|
||||
def _patched_new_conn(self, connection_pool_class, connection_remover):
|
||||
new_conn = connection_pool_class._new_conn
|
||||
@functools.wraps(new_conn)
|
||||
def patched_new_conn(pool):
|
||||
new_connection = new_conn(pool)
|
||||
connection_remover.add_connection_to_pool_entry(pool, new_connection)
|
||||
return new_connection
|
||||
return patched_new_conn
|
||||
|
||||
def _urllib3(self):
|
||||
try:
|
||||
import urllib3.connectionpool as cpool
|
||||
except ImportError: # pragma: no cover
|
||||
return ()
|
||||
from .stubs import urllib3_stubs
|
||||
return self._urllib3_patchers(cpool, urllib3_stubs)
|
||||
|
||||
@_build_patchers_from_mock_triples_decorator
|
||||
def _httplib2(self):
|
||||
try:
|
||||
import httplib2 as cpool
|
||||
except ImportError: # pragma: no cover
|
||||
pass
|
||||
else:
|
||||
from .stubs.httplib2_stubs import VCRHTTPConnectionWithTimeout
|
||||
from .stubs.httplib2_stubs import VCRHTTPSConnectionWithTimeout
|
||||
|
||||
yield cpool, 'HTTPConnectionWithTimeout', VCRHTTPConnectionWithTimeout
|
||||
yield cpool, 'HTTPSConnectionWithTimeout', VCRHTTPSConnectionWithTimeout
|
||||
yield cpool, 'SCHEME_TO_CONNECTION', {'http': VCRHTTPConnectionWithTimeout,
|
||||
'https': VCRHTTPSConnectionWithTimeout}
|
||||
|
||||
@_build_patchers_from_mock_triples_decorator
|
||||
def _boto(self):
|
||||
try:
|
||||
import boto.https_connection as cpool
|
||||
except ImportError: # pragma: no cover
|
||||
pass
|
||||
else:
|
||||
from .stubs.boto_stubs import VCRCertValidatingHTTPSConnection
|
||||
yield cpool, 'CertValidatingHTTPSConnection', VCRCertValidatingHTTPSConnection
|
||||
|
||||
@_build_patchers_from_mock_triples_decorator
|
||||
def _tornado(self):
|
||||
try:
|
||||
import tornado.httpclient as http
|
||||
import tornado.simple_httpclient as simple
|
||||
except ImportError: # pragma: no cover
|
||||
pass
|
||||
else:
|
||||
from .stubs.tornado_stubs import VCRAsyncHTTPClient
|
||||
from .stubs.tornado_stubs import VCRSimpleAsyncHTTPClient
|
||||
|
||||
yield http, 'AsyncHTTPClient', VCRAsyncHTTPClient
|
||||
yield simple, 'SimpleAsyncHTTPClient', VCRSimpleAsyncHTTPClient
|
||||
try:
|
||||
import tornado.curl_httpclient as curl
|
||||
except ImportError: # pragma: no cover
|
||||
pass
|
||||
else:
|
||||
from .stubs.tornado_stubs import VCRCurlAsyncHTTPClient
|
||||
yield curl, 'CurlAsyncHTTPClient', VCRCurlAsyncHTTPClient
|
||||
|
||||
def _urllib3_patchers(self, cpool, stubs):
|
||||
http_connection_remover = ConnectionRemover(
|
||||
self._get_cassette_subclass(stubs.VCRRequestsHTTPConnection)
|
||||
)
|
||||
https_connection_remover = ConnectionRemover(
|
||||
self._get_cassette_subclass(stubs.VCRRequestsHTTPSConnection)
|
||||
)
|
||||
mock_triples = (
|
||||
(cpool, 'VerifiedHTTPSConnection', stubs.VCRRequestsHTTPSConnection),
|
||||
(cpool, 'VerifiedHTTPSConnection', stubs.VCRRequestsHTTPSConnection),
|
||||
(cpool, 'HTTPConnection', stubs.VCRRequestsHTTPConnection),
|
||||
(cpool, 'HTTPSConnection', stubs.VCRRequestsHTTPSConnection),
|
||||
(cpool, 'is_connection_dropped', mock.Mock(return_value=False)), # Needed on Windows only
|
||||
(cpool.HTTPConnectionPool, 'ConnectionCls', stubs.VCRRequestsHTTPConnection),
|
||||
(cpool.HTTPSConnectionPool, 'ConnectionCls', stubs.VCRRequestsHTTPSConnection),
|
||||
)
|
||||
# These handle making sure that sessions only use the
|
||||
# connections of the appropriate type.
|
||||
mock_triples += ((cpool.HTTPConnectionPool, '_get_conn',
|
||||
self._patched_get_conn(cpool.HTTPConnectionPool,
|
||||
lambda : cpool.HTTPConnection)),
|
||||
(cpool.HTTPSConnectionPool, '_get_conn',
|
||||
self._patched_get_conn(cpool.HTTPSConnectionPool,
|
||||
lambda : cpool.HTTPSConnection)),
|
||||
(cpool.HTTPConnectionPool, '_new_conn',
|
||||
self._patched_new_conn(cpool.HTTPConnectionPool,
|
||||
http_connection_remover)),
|
||||
(cpool.HTTPSConnectionPool, '_new_conn',
|
||||
self._patched_new_conn(cpool.HTTPSConnectionPool,
|
||||
https_connection_remover)))
|
||||
|
||||
return itertools.chain(self._build_patchers_from_mock_triples(mock_triples),
|
||||
(http_connection_remover, https_connection_remover))
|
||||
|
||||
|
||||
class ConnectionRemover(object):
|
||||
|
||||
def __init__(self, connection_class):
|
||||
self._connection_class = connection_class
|
||||
self._connection_pool_to_connections = {}
|
||||
|
||||
def add_connection_to_pool_entry(self, pool, connection):
|
||||
if isinstance(connection, self._connection_class):
|
||||
self._connection_pool_to_connections.setdefault(pool, set()).add(connection)
|
||||
|
||||
def remove_connection_to_pool_entry(self, pool, connection):
|
||||
if isinstance(connection, self._connection_class):
|
||||
self._connection_pool_to_connections[self._connection_class].remove(connection)
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, *args):
|
||||
for pool, connections in self._connection_pool_to_connections.items():
|
||||
readd_connections = []
|
||||
while pool.pool and not pool.pool.empty() and connections:
|
||||
connection = pool.pool.get()
|
||||
if isinstance(connection, self._connection_class):
|
||||
connections.remove(connection)
|
||||
else:
|
||||
readd_connections.append(connection)
|
||||
for connection in readd_connections:
|
||||
pool._put_conn(connection)
|
||||
|
||||
|
||||
def reset_patchers():
|
||||
yield mock.patch.object(httplib, 'HTTPConnection', _HTTPConnection)
|
||||
yield mock.patch.object(httplib, 'HTTPSConnection', _HTTPSConnection)
|
||||
try:
|
||||
import requests.packages.urllib3.connectionpool as cpool
|
||||
from .stubs.requests_stubs import VCRVerifiedHTTPSConnection
|
||||
cpool.VerifiedHTTPSConnection = VCRVerifiedHTTPSConnection
|
||||
cpool.VerifiedHTTPSConnection.cassette = cassette
|
||||
cpool.HTTPConnection = VCRHTTPConnection
|
||||
cpool.HTTPConnection.cassette = cassette
|
||||
# patch requests v2.x
|
||||
cpool.HTTPConnectionPool.ConnectionCls = VCRHTTPConnection
|
||||
cpool.HTTPConnectionPool.cassette = cassette
|
||||
cpool.HTTPSConnectionPool.ConnectionCls = VCRHTTPSConnection
|
||||
cpool.HTTPSConnectionPool.cassette = cassette
|
||||
except ImportError: # pragma: no cover
|
||||
pass
|
||||
|
||||
# patch urllib3
|
||||
try:
|
||||
import urllib3.connectionpool as cpool
|
||||
from .stubs.urllib3_stubs import VCRVerifiedHTTPSConnection
|
||||
cpool.VerifiedHTTPSConnection = VCRVerifiedHTTPSConnection
|
||||
cpool.VerifiedHTTPSConnection.cassette = cassette
|
||||
cpool.HTTPConnection = VCRHTTPConnection
|
||||
cpool.HTTPConnection.cassette = cassette
|
||||
except ImportError: # pragma: no cover
|
||||
pass
|
||||
|
||||
# patch httplib2
|
||||
try:
|
||||
import httplib2 as cpool
|
||||
from .stubs.httplib2_stubs import VCRHTTPConnectionWithTimeout
|
||||
from .stubs.httplib2_stubs import VCRHTTPSConnectionWithTimeout
|
||||
cpool.HTTPConnectionWithTimeout = VCRHTTPConnectionWithTimeout
|
||||
cpool.HTTPSConnectionWithTimeout = VCRHTTPSConnectionWithTimeout
|
||||
cpool.SCHEME_TO_CONNECTION = {
|
||||
'http': VCRHTTPConnectionWithTimeout,
|
||||
'https': VCRHTTPSConnectionWithTimeout
|
||||
}
|
||||
except ImportError: # pragma: no cover
|
||||
pass
|
||||
|
||||
|
||||
def reset():
|
||||
'''Undo all the patching'''
|
||||
httplib.HTTPConnection = _HTTPConnection
|
||||
httplib.HTTPSConnection = _HTTPSConnection
|
||||
try:
|
||||
import requests.packages.urllib3.connectionpool as cpool
|
||||
else:
|
||||
# unpatch requests v1.x
|
||||
cpool.VerifiedHTTPSConnection = _VerifiedHTTPSConnection
|
||||
cpool.HTTPConnection = _cpoolHTTPConnection
|
||||
yield mock.patch.object(cpool, 'VerifiedHTTPSConnection', _VerifiedHTTPSConnection)
|
||||
yield mock.patch.object(cpool, 'HTTPConnection', _cpoolHTTPConnection)
|
||||
# unpatch requests v2.x
|
||||
cpool.HTTPConnectionPool.ConnectionCls = _cpoolHTTPConnection
|
||||
cpool.HTTPSConnection = _cpoolHTTPSConnection
|
||||
cpool.HTTPSConnectionPool.ConnectionCls = _cpoolHTTPSConnection
|
||||
except ImportError: # pragma: no cover
|
||||
pass
|
||||
if hasattr(cpool.HTTPConnectionPool, 'ConnectionCls'):
|
||||
yield mock.patch.object(cpool.HTTPConnectionPool, 'ConnectionCls',
|
||||
_cpoolHTTPConnection)
|
||||
yield mock.patch.object(cpool.HTTPSConnectionPool, 'ConnectionCls',
|
||||
_cpoolHTTPSConnection)
|
||||
|
||||
if hasattr(cpool, 'HTTPSConnection'):
|
||||
yield mock.patch.object(cpool, 'HTTPSConnection', _cpoolHTTPSConnection)
|
||||
|
||||
try:
|
||||
import urllib3.connectionpool as cpool
|
||||
cpool.VerifiedHTTPSConnection = _VerifiedHTTPSConnection
|
||||
cpool.HTTPConnection = _HTTPConnection
|
||||
cpool.HTTPSConnection = _HTTPSConnection
|
||||
cpool.HTTPConnectionPool.ConnectionCls = _HTTPConnection
|
||||
cpool.HTTPSConnectionPool.ConnectionCls = _HTTPSConnection
|
||||
except ImportError: # pragma: no cover
|
||||
pass
|
||||
else:
|
||||
yield mock.patch.object(cpool, 'VerifiedHTTPSConnection', _VerifiedHTTPSConnection)
|
||||
yield mock.patch.object(cpool, 'HTTPConnection', _HTTPConnection)
|
||||
yield mock.patch.object(cpool, 'HTTPSConnection', _HTTPSConnection)
|
||||
if hasattr(cpool.HTTPConnectionPool, 'ConnectionCls'):
|
||||
yield mock.patch.object(cpool.HTTPConnectionPool, 'ConnectionCls', _HTTPConnection)
|
||||
yield mock.patch.object(cpool.HTTPSConnectionPool, 'ConnectionCls', _HTTPSConnection)
|
||||
|
||||
try:
|
||||
import httplib2 as cpool
|
||||
cpool.HTTPConnectionWithTimeout = _HTTPConnectionWithTimeout
|
||||
cpool.HTTPSConnectionWithTimeout = _HTTPSConnectionWithTimeout
|
||||
cpool.SCHEME_TO_CONNECTION = _SCHEME_TO_CONNECTION
|
||||
except ImportError: # pragma: no cover
|
||||
pass
|
||||
else:
|
||||
yield mock.patch.object(cpool, 'HTTPConnectionWithTimeout', _HTTPConnectionWithTimeout)
|
||||
yield mock.patch.object(cpool, 'HTTPSConnectionWithTimeout', _HTTPSConnectionWithTimeout)
|
||||
yield mock.patch.object(cpool, 'SCHEME_TO_CONNECTION', _SCHEME_TO_CONNECTION)
|
||||
|
||||
try:
|
||||
import boto.https_connection as cpool
|
||||
except ImportError: # pragma: no cover
|
||||
pass
|
||||
else:
|
||||
yield mock.patch.object(cpool, 'CertValidatingHTTPSConnection',
|
||||
_CertValidatingHTTPSConnection)
|
||||
|
||||
try:
|
||||
import tornado.httpclient as http
|
||||
import tornado.simple_httpclient as simple
|
||||
except ImportError: # pragma: no cover
|
||||
pass
|
||||
else:
|
||||
yield mock.patch.object(http, 'AsyncHTTPClient', _AsyncHTTPClient)
|
||||
yield mock.patch.object(simple, 'SimpleAsyncHTTPClient', _SimpleAsyncHTTPClient)
|
||||
try:
|
||||
import tornado.curl_httpclient as curl
|
||||
except ImportError: # pragma: no cover
|
||||
pass
|
||||
else:
|
||||
yield mock.patch.object(curl, 'CurlAsyncHTTPClient', _CurlAsyncHTTPClient)
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def force_reset():
|
||||
with contextlib.ExitStack() as exit_stack:
|
||||
for patcher in reset_patchers():
|
||||
exit_stack.enter_context(patcher)
|
||||
yield
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
from .persisters.filesystem import FilesystemPersister
|
||||
from .serialize import serialize, deserialize
|
||||
|
||||
|
||||
def load_cassette(cassette_path, serializer):
|
||||
with open(cassette_path) as f:
|
||||
return serializer.deserialize(f.read())
|
||||
cassette_content = f.read()
|
||||
cassette = deserialize(cassette_content, serializer)
|
||||
return cassette
|
||||
|
||||
|
||||
def save_cassette(cassette_path, cassette_dict, serializer):
|
||||
data = serializer.serialize(cassette_dict)
|
||||
data = serialize(cassette_dict, serializer)
|
||||
FilesystemPersister.write(cassette_path, data)
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import tempfile
|
||||
import os
|
||||
|
||||
|
||||
|
||||
120
vcr/request.py
120
vcr/request.py
@@ -1,55 +1,105 @@
|
||||
from six import BytesIO, text_type
|
||||
from six.moves.urllib.parse import urlparse, parse_qsl
|
||||
|
||||
|
||||
class Request(object):
|
||||
"""
|
||||
VCR's representation of a request.
|
||||
|
||||
def __init__(self, protocol, host, port, method, path, body, headers):
|
||||
self.protocol = protocol
|
||||
self.host = host
|
||||
self.port = port
|
||||
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 __init__(self, method, uri, body, headers):
|
||||
self.method = method
|
||||
self.path = path
|
||||
self.body = body
|
||||
# make headers a frozenset so it will be hashable
|
||||
self.headers = frozenset(headers.items())
|
||||
|
||||
def add_header(self, key, value):
|
||||
tmp = dict(self.headers)
|
||||
tmp[key] = value
|
||||
self.headers = frozenset(tmp.iteritems())
|
||||
self.uri = uri
|
||||
self._was_file = hasattr(body, 'read')
|
||||
if self._was_file:
|
||||
self.body = body.read()
|
||||
else:
|
||||
self.body = body
|
||||
self.headers = {}
|
||||
for key in headers:
|
||||
self.add_header(key, headers[key])
|
||||
|
||||
@property
|
||||
def body(self):
|
||||
return BytesIO(self._body) if self._was_file else self._body
|
||||
|
||||
@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
|
||||
|
||||
@property
|
||||
def scheme(self):
|
||||
return urlparse(self.uri).scheme
|
||||
|
||||
@property
|
||||
def host(self):
|
||||
return urlparse(self.uri).hostname
|
||||
|
||||
@property
|
||||
def port(self):
|
||||
parse_uri = urlparse(self.uri)
|
||||
port = parse_uri.port
|
||||
if port is None:
|
||||
port = {'https': 443, 'http': 80}[parse_uri.scheme]
|
||||
return port
|
||||
|
||||
@property
|
||||
def path(self):
|
||||
return urlparse(self.uri).path
|
||||
|
||||
@property
|
||||
def query(self):
|
||||
q = urlparse(self.uri).query
|
||||
return sorted(parse_qsl(q))
|
||||
|
||||
# alias for backwards compatibility
|
||||
@property
|
||||
def url(self):
|
||||
return "{0}://{1}{2}".format(self.protocol, self.host, self.path)
|
||||
return self.uri
|
||||
|
||||
def __key(self):
|
||||
return (
|
||||
self.host,
|
||||
self.port,
|
||||
self.method,
|
||||
self.path,
|
||||
self.body,
|
||||
self.headers
|
||||
)
|
||||
|
||||
def __hash__(self):
|
||||
return hash(self.__key())
|
||||
|
||||
def __eq__(self, other):
|
||||
return hash(self) == hash(other)
|
||||
# alias for backwards compatibility
|
||||
@property
|
||||
def protocol(self):
|
||||
return self.scheme
|
||||
|
||||
def __str__(self):
|
||||
return "<Request ({0}) {1}>".format(self.method, self.url)
|
||||
return "<Request ({0}) {1}>".format(self.method, self.uri)
|
||||
|
||||
def __repr__(self):
|
||||
return self.__str__()
|
||||
|
||||
def _to_dict(self):
|
||||
return {
|
||||
'protocol': self.protocol,
|
||||
'host': self.host,
|
||||
'port': self.port,
|
||||
'method': self.method,
|
||||
'path': self.path,
|
||||
'uri': self.uri,
|
||||
'body': self.body,
|
||||
'headers': self.headers,
|
||||
'headers': dict(((k, [v]) for k, v in self.headers.items())),
|
||||
}
|
||||
|
||||
@classmethod
|
||||
|
||||
63
vcr/serialize.py
Normal file
63
vcr/serialize.py
Normal file
@@ -0,0 +1,63 @@
|
||||
from vcr.serializers import compat
|
||||
from vcr.request import Request
|
||||
import yaml
|
||||
|
||||
# version 1 cassettes started with VCR 1.0.x.
|
||||
# Before 1.0.x, there was no versioning.
|
||||
CASSETTE_FORMAT_VERSION = 1
|
||||
|
||||
"""
|
||||
Just a general note on the serialization philosophy here:
|
||||
I prefer cassettes to be human-readable if possible. Yaml serializes
|
||||
bytestrings to !!binary, which isn't readable, so I would like to serialize to
|
||||
strings and from strings, which yaml will encode as utf-8 automatically.
|
||||
All the internal HTTP stuff expects bytestrings, so this whole serialization
|
||||
process feels backwards.
|
||||
|
||||
Serializing: bytestring -> string (yaml persists to utf-8)
|
||||
Deserializing: string (yaml converts from utf-8) -> bytestring
|
||||
"""
|
||||
|
||||
|
||||
def _looks_like_an_old_cassette(data):
|
||||
return isinstance(data, list) and len(data) and 'request' in data[0]
|
||||
|
||||
|
||||
def _warn_about_old_cassette_format():
|
||||
raise ValueError(
|
||||
"Your cassette files were generated in an older version "
|
||||
"of VCR. Delete your cassettes or run the migration script."
|
||||
"See http://git.io/mHhLBg for more details."
|
||||
)
|
||||
|
||||
|
||||
def deserialize(cassette_string, serializer):
|
||||
try:
|
||||
data = serializer.deserialize(cassette_string)
|
||||
# Old cassettes used to use yaml object thingy so I have to
|
||||
# check for some fairly stupid exceptions here
|
||||
except (ImportError, yaml.constructor.ConstructorError):
|
||||
_warn_about_old_cassette_format()
|
||||
if _looks_like_an_old_cassette(data):
|
||||
_warn_about_old_cassette_format()
|
||||
|
||||
requests = [Request._from_dict(r['request']) for r in data['interactions']]
|
||||
responses = [
|
||||
compat.convert_to_bytes(r['response']) for r in data['interactions']
|
||||
]
|
||||
return requests, responses
|
||||
|
||||
|
||||
def serialize(cassette_dict, serializer):
|
||||
interactions = ([{
|
||||
'request': request._to_dict(),
|
||||
'response': compat.convert_to_unicode(response),
|
||||
} for request, response in zip(
|
||||
cassette_dict['requests'],
|
||||
cassette_dict['responses'],
|
||||
)])
|
||||
data = {
|
||||
'version': CASSETTE_FORMAT_VERSION,
|
||||
'interactions': interactions,
|
||||
}
|
||||
return serializer.serialize(data)
|
||||
@@ -2,33 +2,15 @@ import six
|
||||
|
||||
|
||||
def convert_to_bytes(resp):
|
||||
resp = convert_headers_to_bytes(resp)
|
||||
resp = convert_body_to_bytes(resp)
|
||||
return resp
|
||||
|
||||
|
||||
def convert_to_unicode(resp):
|
||||
resp = convert_headers_to_unicode(resp)
|
||||
resp = convert_body_to_unicode(resp)
|
||||
return resp
|
||||
|
||||
|
||||
def convert_headers_to_bytes(resp):
|
||||
try:
|
||||
resp['headers'] = [h.encode('utf-8') for h in resp['headers']]
|
||||
except (KeyError, TypeError):
|
||||
pass
|
||||
return resp
|
||||
|
||||
|
||||
def convert_headers_to_unicode(resp):
|
||||
try:
|
||||
resp['headers'] = [h.decode('utf-8') for h in resp['headers']]
|
||||
except (KeyError, TypeError):
|
||||
pass
|
||||
return resp
|
||||
|
||||
|
||||
def convert_body_to_bytes(resp):
|
||||
"""
|
||||
If the request body is a string, encode it to bytes (for python3 support)
|
||||
|
||||
@@ -1,30 +1,24 @@
|
||||
from vcr.request import Request
|
||||
from . import compat
|
||||
try:
|
||||
import simplejson as json
|
||||
except ImportError:
|
||||
import json
|
||||
|
||||
|
||||
def _json_default(obj):
|
||||
if isinstance(obj, frozenset):
|
||||
return dict(obj)
|
||||
return obj
|
||||
|
||||
|
||||
def deserialize(cassette_string):
|
||||
data = json.loads(cassette_string)
|
||||
requests = [Request._from_dict(r['request']) for r in data]
|
||||
responses = [compat.convert_to_bytes(r['response']) for r in data]
|
||||
return requests, responses
|
||||
return json.loads(cassette_string)
|
||||
|
||||
|
||||
def serialize(cassette_dict):
|
||||
data = ([{
|
||||
'request': request._to_dict(),
|
||||
'response': compat.convert_to_unicode(response),
|
||||
} for request, response in zip(
|
||||
cassette_dict['requests'],
|
||||
cassette_dict['responses']
|
||||
)])
|
||||
return json.dumps(data, indent=4, default=_json_default)
|
||||
try:
|
||||
return json.dumps(cassette_dict, indent=4)
|
||||
except UnicodeDecodeError as original:
|
||||
raise UnicodeDecodeError(
|
||||
original.encoding,
|
||||
b"Error serializing cassette to JSON",
|
||||
original.start,
|
||||
original.end,
|
||||
original.args[-1] +
|
||||
("Does this HTTP interaction contain binary data? "
|
||||
"If so, use a different serializer (like the yaml serializer) "
|
||||
"for this request?")
|
||||
)
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import sys
|
||||
import yaml
|
||||
from . import compat
|
||||
|
||||
# Use the libYAML versions if possible
|
||||
try:
|
||||
@@ -8,49 +6,10 @@ try:
|
||||
except ImportError:
|
||||
from yaml import Loader, Dumper
|
||||
|
||||
"""
|
||||
Just a general note on the serialization philosophy here:
|
||||
I prefer cassettes to be human-readable if possible. Yaml serializes
|
||||
bytestrings to !!binary, which isn't readable, so I would like to serialize to
|
||||
strings and from strings, which yaml will encode as utf-8 automatically.
|
||||
All the internal HTTP stuff expects bytestrings, so this whole serialization
|
||||
process feels backwards.
|
||||
|
||||
Serializing: bytestring -> string (yaml persists to utf-8)
|
||||
Deserializing: string (yaml converts from utf-8) -> bytestring
|
||||
"""
|
||||
|
||||
|
||||
def _restore_frozenset():
|
||||
"""
|
||||
Restore __builtin__.frozenset for cassettes serialized in python2 but
|
||||
deserialized in python3 and builtins.frozenset for cassettes serialized
|
||||
in python3 and deserialized in python2
|
||||
"""
|
||||
|
||||
if '__builtin__' not in sys.modules:
|
||||
import builtins
|
||||
sys.modules['__builtin__'] = builtins
|
||||
|
||||
if 'builtins' not in sys.modules:
|
||||
sys.modules['builtins'] = sys.modules['__builtin__']
|
||||
|
||||
|
||||
def deserialize(cassette_string):
|
||||
_restore_frozenset()
|
||||
data = yaml.load(cassette_string, Loader=Loader)
|
||||
requests = [r['request'] for r in data]
|
||||
responses = [r['response'] for r in data]
|
||||
responses = [compat.convert_to_bytes(r['response']) for r in data]
|
||||
return requests, responses
|
||||
return yaml.load(cassette_string, Loader=Loader)
|
||||
|
||||
|
||||
def serialize(cassette_dict):
|
||||
data = ([{
|
||||
'request': request,
|
||||
'response': response,
|
||||
} for request, response in zip(
|
||||
cassette_dict['requests'],
|
||||
[compat.convert_to_unicode(r) for r in cassette_dict['responses']],
|
||||
)])
|
||||
return yaml.dump(data, Dumper=Dumper)
|
||||
return yaml.dump(cassette_dict, Dumper=Dumper)
|
||||
|
||||
@@ -4,6 +4,7 @@ try:
|
||||
import http.client
|
||||
except ImportError:
|
||||
pass
|
||||
import logging
|
||||
import six
|
||||
from six.moves.http_client import (
|
||||
HTTPConnection,
|
||||
@@ -16,26 +17,50 @@ from vcr.request import Request
|
||||
from vcr.errors import CannotOverwriteExistingCassetteException
|
||||
from . import compat
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
def parse_headers_backwards_compat(header_dict):
|
||||
|
||||
class VCRFakeSocket(object):
|
||||
"""
|
||||
In vcr 0.6.0, I changed the cassettes to store
|
||||
headers as a list instead of a dict. This method
|
||||
parses the old dictionary-style headers for
|
||||
backwards-compatability reasons.
|
||||
A socket that doesn't do anything!
|
||||
Used when playing back casssettes, when there
|
||||
is no actual open socket.
|
||||
"""
|
||||
msg = HTTPMessage(BytesIO(""))
|
||||
for key, val in header_dict.items():
|
||||
msg.addheader(key, val)
|
||||
msg.headers.append("{0}:{1}".format(key, val))
|
||||
return msg
|
||||
|
||||
def close(self):
|
||||
pass
|
||||
|
||||
def settimeout(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
def fileno(self):
|
||||
"""
|
||||
This is kinda crappy. requests will watch
|
||||
this descriptor and make sure it's not closed.
|
||||
Return file descriptor 0 since that's stdin.
|
||||
"""
|
||||
return 0 # wonder how bad this is....
|
||||
|
||||
|
||||
def parse_headers(header_list):
|
||||
if isinstance(header_list, dict):
|
||||
return parse_headers_backwards_compat(header_list)
|
||||
headers = b"".join(header_list) + b"\r\n"
|
||||
return compat.get_httpmessage(headers)
|
||||
"""
|
||||
Convert headers from our serialized dict with lists for keys to a
|
||||
HTTPMessage
|
||||
"""
|
||||
header_string = b""
|
||||
for key, values in header_list.items():
|
||||
for v in values:
|
||||
header_string += \
|
||||
key.encode('utf-8') + b":" + v.encode('utf-8') + b"\r\n"
|
||||
return compat.get_httpmessage(header_string)
|
||||
|
||||
|
||||
def serialize_headers(response):
|
||||
out = {}
|
||||
for key, values in compat.get_headers(response.msg):
|
||||
out.setdefault(key, [])
|
||||
out[key].extend(values)
|
||||
return out
|
||||
|
||||
|
||||
class VCRHTTPResponse(HTTPResponse):
|
||||
@@ -51,7 +76,15 @@ class VCRHTTPResponse(HTTPResponse):
|
||||
self._closed = False
|
||||
|
||||
headers = self.recorded_response['headers']
|
||||
self.msg = parse_headers(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
|
||||
|
||||
@@ -83,29 +116,57 @@ class VCRHTTPResponse(HTTPResponse):
|
||||
|
||||
def getheaders(self):
|
||||
message = parse_headers(self.recorded_response['headers'])
|
||||
return compat.get_header_items(message)
|
||||
return list(compat.get_header_items(message))
|
||||
|
||||
def getheader(self, header, default=None):
|
||||
headers = dict(((k, v) for k, v in self.getheaders()))
|
||||
return headers.get(header, default)
|
||||
values = [v for (k, v) in self.getheaders() if k.lower() == header.lower()]
|
||||
|
||||
if values:
|
||||
return ', '.join(values)
|
||||
else:
|
||||
return default
|
||||
|
||||
|
||||
class VCRConnection:
|
||||
class VCRConnection(object):
|
||||
# A reference to the cassette that's currently being patched in
|
||||
cassette = None
|
||||
|
||||
def _port_postfix(self):
|
||||
"""
|
||||
Returns empty string for the default port and ':port' otherwise
|
||||
"""
|
||||
port = self.real_connection.port
|
||||
default_port = {'https': 443, 'http': 80}[self._protocol]
|
||||
return ':{0}'.format(port) if port != default_port else ''
|
||||
|
||||
def _uri(self, url):
|
||||
"""Returns request absolute URI"""
|
||||
uri = "{0}://{1}{2}{3}".format(
|
||||
self._protocol,
|
||||
self.real_connection.host,
|
||||
self._port_postfix(),
|
||||
url,
|
||||
)
|
||||
return uri
|
||||
|
||||
def _url(self, uri):
|
||||
"""Returns request selector url from absolute URI"""
|
||||
prefix = "{0}://{1}{2}".format(
|
||||
self._protocol,
|
||||
self.real_connection.host,
|
||||
self._port_postfix(),
|
||||
)
|
||||
return uri.replace(prefix, '', 1)
|
||||
|
||||
def request(self, method, url, body=None, headers=None):
|
||||
'''Persist the request metadata in self._vcr_request'''
|
||||
|
||||
self._vcr_request = Request(
|
||||
protocol=self._protocol,
|
||||
host=self.real_connection.host,
|
||||
port=self.real_connection.port,
|
||||
method=method,
|
||||
path=url,
|
||||
uri=self._uri(url),
|
||||
body=body,
|
||||
headers=headers or {}
|
||||
)
|
||||
log.debug('Got {0}'.format(self._vcr_request))
|
||||
|
||||
# Note: The request may not actually be finished at this point, so
|
||||
# I'm not sending the actual request until getresponse(). This
|
||||
@@ -119,14 +180,12 @@ class VCRConnection:
|
||||
of putheader() calls.
|
||||
"""
|
||||
self._vcr_request = Request(
|
||||
protocol=self._protocol,
|
||||
host=self.real_connection.host,
|
||||
port=self.real_connection.port,
|
||||
method=method,
|
||||
path=url,
|
||||
uri=self._uri(url),
|
||||
body="",
|
||||
headers={}
|
||||
)
|
||||
log.debug('Got {0}'.format(self._vcr_request))
|
||||
|
||||
def putheader(self, header, *values):
|
||||
for value in values:
|
||||
@@ -145,40 +204,57 @@ class VCRConnection:
|
||||
# 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):
|
||||
'''Retrieve a the response'''
|
||||
def getresponse(self, _=False, **kwargs):
|
||||
'''Retrieve the response'''
|
||||
# Check to see if the cassette has a response for this request. If so,
|
||||
# then return it
|
||||
if self._vcr_request in self.cassette and \
|
||||
self.cassette.record_mode != "all" and \
|
||||
self.cassette.rewound:
|
||||
if self.cassette.can_play_response_for(self._vcr_request):
|
||||
log.info(
|
||||
"Playing response for {0} from cassette".format(
|
||||
self._vcr_request
|
||||
)
|
||||
)
|
||||
response = self.cassette.play_response(self._vcr_request)
|
||||
return VCRHTTPResponse(response)
|
||||
else:
|
||||
if self.cassette.write_protected:
|
||||
if self.cassette.write_protected and self.cassette.filter_request(
|
||||
self._vcr_request
|
||||
):
|
||||
raise CannotOverwriteExistingCassetteException(
|
||||
"No match for the request (%r) was found. "
|
||||
"Can't overwrite existing cassette (%r) in "
|
||||
"your current record mode (%r)."
|
||||
% (self.cassette._path, self.cassette.record_mode)
|
||||
% (self._vcr_request, self.cassette._path,
|
||||
self.cassette.record_mode)
|
||||
)
|
||||
|
||||
# Otherwise, we should send the request, then get the response
|
||||
# and return it.
|
||||
|
||||
self.real_connection.request(
|
||||
method=self._vcr_request.method,
|
||||
url=self._vcr_request.path,
|
||||
body=self._vcr_request.body,
|
||||
headers=dict(self._vcr_request.headers or {})
|
||||
log.info(
|
||||
"{0} not in cassette, sending to real server".format(
|
||||
self._vcr_request
|
||||
)
|
||||
)
|
||||
# This is imported here to avoid circular import.
|
||||
# TODO(@IvanMalison): Refactor to allow normal import.
|
||||
from vcr.patch import force_reset
|
||||
with force_reset():
|
||||
self.real_connection.request(
|
||||
method=self._vcr_request.method,
|
||||
url=self._url(self._vcr_request.uri),
|
||||
body=self._vcr_request.body,
|
||||
headers=self._vcr_request.headers,
|
||||
)
|
||||
|
||||
# get the response
|
||||
response = self.real_connection.getresponse()
|
||||
@@ -189,7 +265,7 @@ class VCRConnection:
|
||||
'code': response.status,
|
||||
'message': response.reason
|
||||
},
|
||||
'headers': compat.get_headers(response),
|
||||
'headers': serialize_headers(response),
|
||||
'body': {'string': response.read()},
|
||||
}
|
||||
self.cassette.append(self._vcr_request, response)
|
||||
@@ -207,9 +283,7 @@ class VCRConnection:
|
||||
"""
|
||||
|
||||
if hasattr(self, '_vcr_request') and \
|
||||
self._vcr_request in self.cassette and \
|
||||
self.cassette.record_mode != "all" and \
|
||||
self.cassette.rewound:
|
||||
self.cassette.can_play_response_for(self._vcr_request):
|
||||
# We already have a response we are going to play, don't
|
||||
# actually connect
|
||||
return
|
||||
@@ -220,14 +294,27 @@ class VCRConnection:
|
||||
|
||||
return self.real_connection.connect(*args, **kwargs)
|
||||
|
||||
@property
|
||||
def sock(self):
|
||||
if self.real_connection.sock:
|
||||
return self.real_connection.sock
|
||||
return VCRFakeSocket()
|
||||
|
||||
@sock.setter
|
||||
def sock(self, value):
|
||||
if self.real_connection.sock:
|
||||
self.real_connection.sock = value
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
if six.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
|
||||
# the reset if you want to see what I mean :)
|
||||
from vcr.patch import install, reset
|
||||
reset()
|
||||
self.real_connection = self._baseclass(*args, **kwargs)
|
||||
install(self.cassette)
|
||||
from vcr.patch import force_reset
|
||||
with force_reset():
|
||||
self.real_connection = self._baseclass(*args, **kwargs)
|
||||
|
||||
|
||||
class VCRHTTPConnection(VCRConnection):
|
||||
@@ -240,3 +327,4 @@ class VCRHTTPSConnection(VCRConnection):
|
||||
'''A Mocked class for HTTPS requests'''
|
||||
_baseclass = HTTPSConnection
|
||||
_protocol = 'https'
|
||||
is_verified = True
|
||||
|
||||
8
vcr/stubs/boto_stubs.py
Normal file
8
vcr/stubs/boto_stubs.py
Normal file
@@ -0,0 +1,8 @@
|
||||
'''Stubs for boto'''
|
||||
|
||||
from boto.https_connection import CertValidatingHTTPSConnection
|
||||
from ..stubs import VCRHTTPSConnection
|
||||
|
||||
|
||||
class VCRCertValidatingHTTPSConnection(VCRHTTPSConnection):
|
||||
_baseclass = CertValidatingHTTPSConnection
|
||||
@@ -21,19 +21,17 @@ def get_header(message, name):
|
||||
|
||||
|
||||
def get_header_items(message):
|
||||
if six.PY3:
|
||||
return dict(message._headers).items()
|
||||
else:
|
||||
return message.dict.items()
|
||||
for (key, values) in get_headers(message):
|
||||
for value in values:
|
||||
yield key, value
|
||||
|
||||
|
||||
def get_headers(response):
|
||||
if six.PY3:
|
||||
header_list = response.msg._headers
|
||||
return [b': '.join((k.encode('utf-8'), v.encode('utf-8'))) + b'\r\n'
|
||||
for k, v in header_list]
|
||||
else:
|
||||
return response.msg.headers
|
||||
def get_headers(message):
|
||||
for key in set(message.keys()):
|
||||
if six.PY3:
|
||||
yield key, message.get_all(key)
|
||||
else:
|
||||
yield key, message.getheaders(key)
|
||||
|
||||
|
||||
def get_httpmessage(headers):
|
||||
|
||||
@@ -47,7 +47,7 @@ class VCRHTTPSConnectionWithTimeout(VCRHTTPSConnection,
|
||||
for kw in unknown_keys:
|
||||
del safe_kwargs[kw]
|
||||
self.proxy_info = kwargs.pop('proxy_info', None)
|
||||
if not 'ca_certs' in kwargs or kwargs['ca_certs'] is None:
|
||||
if 'ca_certs' not in kwargs or kwargs['ca_certs'] is None:
|
||||
try:
|
||||
import httplib2
|
||||
self.ca_certs = httplib2.CA_CERTS
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
'''Stubs for requests'''
|
||||
|
||||
from requests.packages.urllib3.connectionpool import VerifiedHTTPSConnection
|
||||
from ..stubs import VCRHTTPSConnection
|
||||
from requests.packages.urllib3.connectionpool import HTTPConnection, VerifiedHTTPSConnection
|
||||
from ..stubs import VCRHTTPConnection, VCRHTTPSConnection
|
||||
|
||||
# urllib3 defines its own HTTPConnection classes, which requests goes ahead and assumes
|
||||
# you're using. It includes some polyfills for newer features missing in older pythons.
|
||||
|
||||
class VCRVerifiedHTTPSConnection(VCRHTTPSConnection, VerifiedHTTPSConnection):
|
||||
class VCRRequestsHTTPConnection(VCRHTTPConnection, HTTPConnection):
|
||||
_baseclass = HTTPConnection
|
||||
|
||||
class VCRRequestsHTTPSConnection(VCRHTTPSConnection, VerifiedHTTPSConnection):
|
||||
_baseclass = VerifiedHTTPSConnection
|
||||
|
||||
148
vcr/stubs/tornado_stubs.py
Normal file
148
vcr/stubs/tornado_stubs.py
Normal file
@@ -0,0 +1,148 @@
|
||||
'''Stubs for tornado HTTP clients'''
|
||||
from __future__ import absolute_import
|
||||
|
||||
from six import BytesIO
|
||||
|
||||
from tornado import httputil
|
||||
from tornado.httpclient import AsyncHTTPClient
|
||||
from tornado.httpclient import HTTPResponse
|
||||
from tornado.simple_httpclient import SimpleAsyncHTTPClient
|
||||
|
||||
from vcr.errors import CannotOverwriteExistingCassetteException
|
||||
from vcr.request import Request
|
||||
|
||||
|
||||
class _VCRAsyncClient(object):
|
||||
cassette = None
|
||||
|
||||
def __new__(cls, *args, **kwargs):
|
||||
from vcr.patch import force_reset
|
||||
with force_reset():
|
||||
return super(_VCRAsyncClient, cls).__new__(cls, *args, **kwargs)
|
||||
|
||||
def initialize(self, *args, **kwargs):
|
||||
from vcr.patch import force_reset
|
||||
with force_reset():
|
||||
self.real_client = self._baseclass(*args, **kwargs)
|
||||
|
||||
@property
|
||||
def io_loop(self):
|
||||
return self.real_client.io_loop
|
||||
|
||||
@property
|
||||
def _closed(self):
|
||||
return self.real_client._closed
|
||||
|
||||
@property
|
||||
def defaults(self):
|
||||
return self.real_client.defaults
|
||||
|
||||
def close(self):
|
||||
from vcr.patch import force_reset
|
||||
with force_reset():
|
||||
self.real_client.close()
|
||||
|
||||
def fetch_impl(self, request, callback):
|
||||
headers = dict(request.headers)
|
||||
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)
|
||||
),
|
||||
)
|
||||
return callback(response)
|
||||
|
||||
vcr_request = Request(
|
||||
request.method,
|
||||
request.url,
|
||||
request.body,
|
||||
headers,
|
||||
)
|
||||
|
||||
if self.cassette.can_play_response_for(vcr_request):
|
||||
vcr_response = self.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']),
|
||||
)
|
||||
return callback(response)
|
||||
else:
|
||||
if self.cassette.write_protected and self.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, self.cassette._path,
|
||||
self.cassette.record_mode)
|
||||
),
|
||||
)
|
||||
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},
|
||||
}
|
||||
self.cassette.append(vcr_request, vcr_response)
|
||||
return callback(response)
|
||||
|
||||
from vcr.patch import force_reset
|
||||
with force_reset():
|
||||
self.real_client.fetch_impl(request, new_callback)
|
||||
|
||||
|
||||
class VCRAsyncHTTPClient(_VCRAsyncClient, AsyncHTTPClient):
|
||||
_baseclass = AsyncHTTPClient
|
||||
|
||||
|
||||
class VCRSimpleAsyncHTTPClient(_VCRAsyncClient, SimpleAsyncHTTPClient):
|
||||
_baseclass = SimpleAsyncHTTPClient
|
||||
|
||||
|
||||
try:
|
||||
from tornado.curl_httpclient import CurlAsyncHTTPClient
|
||||
except ImportError: # pragma: no cover
|
||||
VCRCurlAsyncHTTPClient = None
|
||||
else:
|
||||
class VCRCurlAsyncHTTPClient(_VCRAsyncClient, CurlAsyncHTTPClient):
|
||||
_baseclass = CurlAsyncHTTPClient
|
||||
@@ -1,8 +1,13 @@
|
||||
'''Stubs for urllib3'''
|
||||
|
||||
from urllib3.connectionpool import VerifiedHTTPSConnection
|
||||
from ..stubs import VCRHTTPSConnection
|
||||
from urllib3.connectionpool import HTTPConnection, VerifiedHTTPSConnection
|
||||
from ..stubs import VCRHTTPConnection, VCRHTTPSConnection
|
||||
|
||||
# urllib3 defines its own HTTPConnection classes. It includes some polyfills
|
||||
# for newer features missing in older pythons.
|
||||
|
||||
class VCRVerifiedHTTPSConnection(VCRHTTPSConnection, VerifiedHTTPSConnection):
|
||||
class VCRRequestsHTTPConnection(VCRHTTPConnection, HTTPConnection):
|
||||
_baseclass = HTTPConnection
|
||||
|
||||
class VCRRequestsHTTPSConnection(VCRHTTPSConnection, VerifiedHTTPSConnection):
|
||||
_baseclass = VerifiedHTTPSConnection
|
||||
|
||||
92
vcr/util.py
Normal file
92
vcr/util.py
Normal file
@@ -0,0 +1,92 @@
|
||||
import collections
|
||||
|
||||
# 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 functions[::-1]:
|
||||
res = function(res)
|
||||
return res
|
||||
return composed
|
||||
|
||||
def read_body(request):
|
||||
if hasattr(request.body, 'read'):
|
||||
return request.body.read()
|
||||
return request.body
|
||||
Reference in New Issue
Block a user