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

Compare commits

...

91 Commits

Author SHA1 Message Date
Josh Peak
78e21aa220 Increment version to v2.1.0 and add Python2.x deprecation warning (#458)
* Increment version and add Python2.x deprecation warning

Change test to actually test no warnings when on python3

flake8 compliant

* fix development status classifier
2019-08-08 18:00:41 +10:00
Arthur Hamon
1b565d3f88 Feat/2.1.0 release changelog documentation (#466)
* chore(changelog): add changelog for 2.1.0 release

* doc(rewind): add documentation a new feature `rewind` that can replay a cassette

* doc(exception): add documentation on the new behavior of the CannotOverwriteExistingCassetteException message
2019-08-08 16:37:55 +10:00
Josh Peak
cc752cf790 Collect and aggregate code coverage across all test environments (#460)
* Collect and aggregate code coverage across all test environments

Add Coveralls badge to readme

Try CodeCov integration

Setup coverage badge for CodeCov

Fix CodeCov Badge

* Set code coverage regression threshold

Attempt to cache bust some boto3 issues on TravisCI

* Disable legacy secure strings

* Skip boto3 tests on TRAVIS_PULL_REQUEST due to credentials being unavailable

Address flake8 issues

Debug TRAVIS_PULL_REQUEST ENV Var

Trying to get boto3 tests to skip on forked PRs

boto3 is not an allowed failure

remove TRAVIS ENV debugging

* remove pytest skipif that wasn't working

* Ignore coverage files
2019-08-06 16:41:42 +10:00
Arthur Hamon
caa3a67bde feat(failure-message): reformat the message with the best requests matches (#459)
The message is less verbose and the display is improved for a better readability.
2019-08-06 09:57:13 +10:00
Laurent Mazuel
e3b7116564 aiohttp headers are case insensitive (#461)
* aiohttp headers are case insensitive

* flakes
2019-07-31 09:17:54 +10:00
Arthur Hamon
9d37210fc8 Merge pull request #451 from neozenith/master
fix IndexError when empty cassette throws CannotOverwriteCassetteException
2019-07-25 08:11:01 +02:00
Josh Peak
857488ee3a Add pytest-cov to check coverage of tox installed library 2019-07-25 11:26:49 +10:00
Josh Peak
9fb8a7b0ba Add guard statement for empty Cassette when returning best match
fix white space flake 8 issue
2019-07-24 20:06:44 +10:00
Josh Peak
77581437f7 fix IndexError when empty cassette throws CannotOverwriteCassetteException 2019-07-24 20:06:44 +10:00
Arthur Hamon
759423a75a Merge pull request #457 from stj/fix-log-formatting
Fix log format bug
2019-07-17 18:05:38 +02:00
Stefan Tjarks
988ac335d4 Fix log format bug
`{}` is not a valid placeholder in log formatting.
2019-07-17 07:46:40 -07:00
Arthur Hamon
200262fb5c Merge pull request #411 from jans-forks/fix/382-boto3
Fixes #382 boto3 compatibility.
2019-07-15 19:38:56 +02:00
Arthur Hamon
8c54ae9826 Merge pull request #456 from kevin1024/fix-aiohttp-stub-to-record-redirects
Fix aiohttp stub to record intermediary redirect responses properly
2019-07-13 19:18:12 +02:00
Arthur Hamon
2d96d35621 delete test_boto3_without_vcr test as it does not test vcr module 2019-07-13 14:13:43 +02:00
Arthur Hamon
6ca7cf3cc6 refactor boto3 integration test using fixtures instead of global variables 2019-07-13 14:13:43 +02:00
Arthur Hamon
5e76e4733d fix real connection on boto3 stub, adding not set parameters manually
Set `assert_hostname` and `cert_reqs` real connection attributes with default values.
2019-07-13 14:13:35 +02:00
Arthur Hamon
14d1454bbf test with latest urllib3 2019-07-13 14:11:59 +02:00
Jan Gazda
4daafcc687 Rename testing user 2019-07-13 12:51:50 +02:00
Jan Gazda
347026f42c Fix #382 - boto3 compatibility
* Add support for PyYaml5.1
* Unpin requests in Tox
* Unpin urllib3 in Tox
* Unpin Flask in Tox
* Add env vars to Tox for boto3 tests
2019-07-13 12:51:50 +02:00
Arthur Hamon
8297d65038 Merge pull request #455 from browniebroke/patch-1
Fix Python version for extra requirement YARL
2019-07-11 13:08:54 +02:00
Luiz Menezes
792c5c4558 Record intermediary redirect responses on aiohttp stub 2019-07-09 18:39:36 -03:00
Bruno Alla
2fa2ca2072 Revert "Remove yarl from extra dependencies: should be there if needed via aiohttp"
This reverts commit 26a4b8df
2019-07-08 14:39:21 +01:00
Bruno Alla
26a4b8df55 Remove yarl from extra dependencies: should be there if needed via aiohttp 2019-07-08 14:12:05 +01:00
Bruno Alla
41e174ccb6 Fix Python version for extra requirement YARL
This was excluded when Python 3.4 support was dropped in #435
2019-07-08 13:29:04 +01:00
Stefan T
a17624a464 Record json kwarg in aiohttp request (#412)
* Record `json` kwarg in aiohttp request

Aiohttp supports `data` and `json` parameters to make a request.
Aiohttp enforces that only one is used make a request.

* Log when aiohttp stub makes request to live server

resolves: #406

* Record aiohttp `auth` request kwarg

Aiohttp request supports basic auth via the `auth` method parameter.
It is set as a request header by aiohttp ClientRequest.
2019-07-05 10:57:39 +10:00
Luiz Menezes
bbab27ed1b Use primitive types when recording cassettes using aiohttp stub (#454) 2019-07-05 10:52:08 +10:00
Arthur Hamon
b92be4e9e5 Merge pull request #386 from valgur/unicode-match-on-body
Fix matching on 'body' failing when Unicode symbols are present
2019-07-02 12:33:21 +02:00
Martin Valgur
c6e7cb12c6 Merge branch 'master' into unicode-match-on-body 2019-07-01 22:09:57 +03:00
Arthur Hamon
d07915ccf6 Merge pull request #408 from StasEvseev/feature/mock_client_json_empty_body
FIX behaviour MockClientResponse.json() if body is empty
2019-07-01 20:38:34 +02:00
Stanislav Evseev
7c14d81ab1 CHANGE: return None from json if body is empty 2019-07-01 14:25:38 +02:00
Arthur Hamon
d682e7b19a Fix all warnings (#449)
* fix typo in pytest.mark.xskip

Change xskip by skipif marker as xskip is an unknown pytest marker.

* fix FileModeWarning

This fix the following warning:
FileModeWarning: Requests has determined the content-length for this request using the binary size of the file: however, the file has been opened in text mode (i.e. without the 'b' flag in the mode). This may lead to an incorrect content-length. In Requests 3.0, support will be removed for files in text mode.

* fix waring "calling yaml.load() without Loader=... is deprecated"
This fix the following warning:
YAMLLoadWarning: calling yaml.load() without Loader=... is deprecated, as the default Loader is unsafe. Please read https://msg.pyyaml.org/load for full details.

* fix collections.abc deprecation warning in python 3.7.

* update Flask dependency in order to get rid of the Request.is_xhr warning

This fix the following warning:
DeprecationWarning: 'Request.is_xhr' is deprecated as of version 0.13 and will be removed in version 1.0. The 'X-Requested-With' header is not standard and is unreliable. You may be able to use 'accept_mimetypes' instead.
2019-07-01 17:45:01 +10:00
Arthur Hamon
2b9498e009 Merge pull request #437 from steinnes/minimal-aiohttp-streamreader-support
Minimal aiohttp streamreader support
2019-07-01 07:16:54 +02:00
Arthur Hamon
eb99a3e36f Merge pull request #442 from addgene/master
fixes #440 VCR responses don't work with Biopython on Python3
2019-06-30 22:54:21 +02:00
Rishab Malik
6c877a1749 added skip decorator for python2 2019-06-28 13:03:55 -04:00
Arthur Hamon
6be6f0236b Merge pull request #415 from jeking3/issue-414
KeyError when replaying proxy tunnel captured sessions (broken in #389, v2.0.0)
2019-06-28 17:20:14 +02:00
Rishab Malik
95c7898b65 [DEV-5365][DEV-5367] VCR responses don't work with Biopython on Python 3 (#1)
* added IOBase methods and fp pointer

* regression test
2019-06-28 09:37:53 -04:00
James E. King III
d4b706334c properly handle tunnel connect uri generation broken in #389 2019-06-28 13:37:14 +00:00
Steinn Eldjárn Sigurðarson
e8a9a65bef Make test use stream output to test MockStream interface 2019-06-28 13:35:05 +00:00
Steinn Eldjárn Sigurðarson
67b03b45c3 Add output option to use response.content stream 2019-06-28 13:34:48 +00:00
Steinn Eldjárn Sigurðarson
6c41b8b723 Use built-in helpers to avoid use of async keywords which cause syntax errors on 2.x 2019-06-28 13:30:14 +00:00
Steinn Eldjárn Sigurðarson
cc55ef5b35 Add test for MockStream on aiohttp 2019-06-28 12:55:54 +00:00
Steinn Eldjárn Sigurðarson
1b6f304421 Add .content to MockClientResponse so code which uses aiohttp request content streams directly can be tested 2019-06-28 12:55:54 +00:00
Arthur Hamon
9039eab916 Merge pull request #418 from PiDelport/patch-1
Doc typo: yml -> yaml
2019-06-28 13:55:10 +02:00
Josh Peak
92e03603ea Merge pull request #446 from neozenith/master
CannotOverwritteCassetteException: Add properties cassette and failed_request
2019-06-28 13:54:43 +10:00
Josh Peak
f8e8b85790 Address testing with tox on windows documentation 2019-06-28 13:29:55 +10:00
Josh Peak
09ed0e911e Add cassette and failed request as properties of thrown CannotOverwriteCassetteException 2019-06-28 11:29:12 +10:00
Josh Peak
0830f6052b Merge pull request #439 from arthurHamon2/master
Add verbosity/explanations on CannotOverwriteExistingCassetteException
2019-06-28 10:45:10 +10:00
Arthur Hamon
829e9b2f1c Merge pull request #444 from hugovk/patch-1
Remove broken Waffle badge
2019-06-27 22:30:10 +02:00
Arthur Hamon
b203fd4113 add reference to pytest-vcr plugin in the documentation 2019-06-27 22:22:10 +02:00
Arthur Hamon
f414e04f49 update the documentation of custom matchers
Add documentation on creating a matcher with an `assert` statement that provides assertion messages in case of failures.
2019-06-27 22:22:10 +02:00
Arthur Hamon
28d9899b9b refactor the 'CannotOverwriteExistingCassetteException' exception, building a more detailed message
The 'CannotOverwriteExistingCassetteException' exception now takes two kwargs, cassette and failed requests, in order to get the request(s) in the cassettes with the less differences and put those details in the exception message.
2019-06-27 22:22:10 +02:00
Arthur Hamon
396c4354e8 add cassette's method to find the most similar request(s) of a request
This method get the requests in the cassettes with the most matchers that succeeds.
2019-06-27 22:22:10 +02:00
Arthur Hamon
0a01f0fb51 change all the matchers with an assert statement and refactor the requests_match function
In order to use the new assert mechanism that returns explicit assertion failure message, all the default matchers does not return a boolean, but only do an assert statement with a basic assertion message (value_1 != value_2).
The requests_match function has been refactored to use the 'get_matchers_results' function in order to have explicit failures that are logged if any.
Many unit tests have been changed as the matchers does not return a boolean value anymore.
Note: Only the matchers "body" and "raw_body" does not have an assertion message, the body values might be big and not useful to be display to spot the differences.
2019-06-27 22:22:10 +02:00
Arthur Hamon
46f5b8a187 add function to get the comparaison result of two request with a list of matchers
The function returns two list:
- the first one is the list of matchers names that have succeeded.
- the second is a list of tuples with the failed matchers names and the related assertion message like this ("matcher_name", "assertion_message").
If the second list is empty, it means that all the matchers have passed.
2019-06-27 22:22:10 +02:00
Arthur Hamon
940dec1dd6 add private function to evaluate a matcher
A matcher can now return other results than a boolean :
- An AssertionError exception meaning that the matcher failed, with the exception we get the assertion failure message.
- None, in case we do an assert in the matcher, meaning that the assertion has passed, the matcher is considered as a success then.
- Boolean that indicates if a matcher failed or not. If there is no match, a boolean does not give any clue what it is the differences compared to the assertion.
2019-06-27 22:22:10 +02:00
Arthur Hamon
de244a968f add function to format the assertion message
This function is used to prettify the assertion message when a matcher failed and return an assertion error.
2019-06-27 22:22:10 +02:00
Arthur Hamon
728dc71a35 Merge pull request #441 from arthurHamon2/update-related-to-new-dependencies-versions
Update tests related to new dependencies versions
2019-06-27 22:03:30 +02:00
Arthur Hamon
bdb74b9841 change requests27 in travis-ci to requests 2019-06-27 21:33:39 +02:00
Arthur Hamon
34f0417dc9 add troubleshooting in contributing section
When I ran the test suite on macOSX, I had some issues regarding SSL configuration, I have documented the error I encounter and the solution to deal with it.
2019-06-27 21:33:39 +02:00
Hugo
86586e8cd9 Remove broken Waffle badge
Waffle shut down on May 16th, 2019 "due to market direction and the acquisition by Broadcom".

https://help.waffle.io/articles/2801857-waffle-shutdown-tl-dr
2019-06-17 11:48:12 +03:00
Arthur Hamon
7724b364aa urllib3 has a new default behavior with PoolManager object creation
By default, from version urllib3=>1.25, urllib3 requires and validates certificates by default when using HTTPS (https://github.com/urllib3/urllib3/blob/master/CHANGES.rst#125-2019-04-22).
Set explicitly that we deactivate certificates validation with cert_reqs=`CERT_NONE`.
2019-06-13 16:04:26 +02:00
Arthur Hamon
c4803dbc4d httplib2 has issues validating certificate with python 2.7, disabling it 2019-06-13 16:04:26 +02:00
Arthur Hamon
a53121b645 do not create tox environment with python2 and aiohttp
aiohttp dependency only works with python 3.5+
2019-06-13 16:04:26 +02:00
Arthur Hamon
78a0a52bd9 fix issue with tests using localhost
The error we have : SSLError: hostname '127.0.0.1' doesn't match either of 'localhost', '127.0.0.1'
This is fixed by adding the `ipaddress` dependency in the tox ini.
2019-06-13 16:04:26 +02:00
Ivan Malison
18977a85d1 Merge pull request #413 from khamidou/add_rewind_method
Add a `rewind` method to reset a cassette.
2019-06-10 17:53:37 -07:00
Luiz Menezes
fb84928ef6 Fix build problems on requests tests due to SSL certificate problems 2019-06-10 16:57:24 -03:00
Luiz Menezes
0b4d92c277 Merge pull request #435 from hugovk/rm-3.4
Drop support for EOL Python 3.4
2019-06-10 15:38:36 -03:00
Hugo
8f4e089200 Upgrade Python syntax with pyupgrade 2019-05-22 17:44:36 +03:00
Hugo
7670e10bc2 Fix Flake8: E117 over-indented 2019-05-22 17:35:15 +03:00
Hugo
dc174c3250 Drop support for EOL Python 3.4 2019-05-22 17:30:45 +03:00
Luiz Menezes
114fcd29b4 Merge pull request #416 from davidwilemski/util-warnings
Fix collections.abc DeprecationWarning
2019-04-06 00:17:32 -03:00
Pi Delport
20e8f4ad41 Doc typo: yml -> yaml 2019-02-04 12:15:23 +02:00
David Wilemski
4e990db32e Fix collections.abc DeprecationWarning
In versions of Python from 3.8 and forward, importing Mapping and
MutableMapping from the collections module will no longer work. This
change will try to import from the collections.abc module, which was
added in Python 3.3, and fall back to the collections module on older
versions of Python.
2019-01-01 15:23:51 -08:00
Karim Hamidou
472bc3aea1 Add a rewind method to reset a cassette. 2018-12-20 12:44:07 +01:00
Luiz Menezes
c74a857aa4 Merge pull request #409 from jxltom/pin-requires-version-for-python34
Pin yarl and multidict version for python34
2018-11-14 09:59:39 -03:00
jxltom
c3705dae9f Fix flake8 in python3 2018-11-14 10:27:38 +08:00
jxltom
6c166482d9 Pin yarl and multidict version for python34 2018-11-14 09:48:49 +08:00
Thomas Grainger
cc9fabf2d9 Merge pull request #379 from adamchainz/patch-1
Update docs on before_record_* callbacks
2018-10-15 04:05:16 +01:00
Thomas Grainger
f77442d87b Merge pull request #400 from felixonmars/patch-1
Fix a typo in vcr/request.py
2018-10-15 04:04:32 +01:00
Thomas Grainger
602112cd87 Merge pull request #404 from kevin1024/text-encoding-errors-kwarg
support ClientResponse.text(errors=) kwarg
2018-10-15 04:04:15 +01:00
Thomas Grainger
4ef5205094 support ClientResponse.text(errors=) kwarg 2018-10-15 03:00:19 +01:00
Felix Yan
0d2f49fe8a Fix a typo in vcr/request.py 2018-10-06 17:21:42 +08:00
Luiz Menezes
8fdc6dbb68 Merge pull request #397 from stj/master
ClientResponse.release isn't a coroutine
2018-10-04 12:04:05 -03:00
Stefan Tjarks
ffc4dca502 ClientResponse.release isn't a coroutine
Therefore it should not be one in the MockClientResponse class.

d0af887e31/aiohttp/client_reqrep.py (L832)
2018-09-26 00:30:11 -07:00
Luiz Menezes
e42746fa88 Bump version to 2.0.1 2018-09-23 15:25:04 -03:00
Luiz Menezes
03b1dd9faa Merge pull request #395 from kevin1024/fix-py34
Fix py34
2018-09-22 23:16:14 -03:00
Luiz Menezes
f2a79d3fcc Add py34 to CI builds 2018-09-22 17:30:21 -03:00
Luiz Menezes
287ea4b06e Fix cassette module to work with py34 2018-09-22 17:29:08 -03:00
Martin Valgur
302ea35d9a Fix matching on 'body' failing when Unicode symbols are present in them
Matching on bodies uses urllib.parse.parse_qs(), which fails to handle
UTF-8+URLEncoded POST bodies when the input is `bytes` rather than `str`,
causing matching to fail..
Fixed this by always doing decode('ascii') on URLEncoded POST bodies first.
2018-09-06 19:41:37 +03:00
Martin Valgur
895850b197 Fix failing unit test on Windows
test_xmlrpclib was failing with "can't pickle thread.lock objects" on Windows.
Other small issues were related to backslashes in paths and different line endings.
2018-09-06 19:41:37 +03:00
Adam Johnson
a9e75a545e Update docs on before_record_* callbacks
Make them a bit more consistent and obvious that returning `None` ignores the request/response.
2018-07-31 09:54:29 +01:00
44 changed files with 1154 additions and 270 deletions

7
.codecov.yml Normal file
View File

@@ -0,0 +1,7 @@
coverage:
status:
project:
default:
target: 75
# Allow 0% coverage regression
threshold: 0

2
.gitignore vendored
View File

@@ -6,6 +6,8 @@ build/
dist/
*.egg/
.coverage
coverage.xml
htmlcov/
*.egg-info/
pytestdebug.log

View File

@@ -1,16 +1,15 @@
language: python
sudo: false
before_install: openssl version
env:
global:
- secure: AifoKzwhjV94cmcQZrdQmqRu/9rkZZvWpwBv1daeAQpLOKFPGsOm3D+x2cSw9+iCfkgDZDfqQVv1kCaFVxTll8v8jTq5SJdqEY0NmGWbj/UkNtShh609oRDsuzLxAEwtVKYjf/h8K2BRea+bl1tGkwZ2vtmYS6dxNlAijjWOfds=
- secure: LBSEg/gMj4u4Hrpo3zs6Y/1mTpd2RtcN49mZIFgTdbJ9IhpiNPqcEt647Lz94F9Eses2x2WbNuKqZKZZReY7QLbEzU1m0nN5jlaKrjcG5NR5clNABfFFyhgc0jBikyS4abAG8jc2efeaTrFuQwdoF4sE8YiVrkiVj2X5Xoi6sBk=
# global:
# - secure: AifoKzwhjV94cmcQZrdQmqRu/9rkZZvWpwBv1daeAQpLOKFPGsOm3D+x2cSw9+iCfkgDZDfqQVv1kCaFVxTll8v8jTq5SJdqEY0NmGWbj/UkNtShh609oRDsuzLxAEwtVKYjf/h8K2BRea+bl1tGkwZ2vtmYS6dxNlAijjWOfds=
# - secure: LBSEg/gMj4u4Hrpo3zs6Y/1mTpd2RtcN49mZIFgTdbJ9IhpiNPqcEt647Lz94F9Eses2x2WbNuKqZKZZReY7QLbEzU1m0nN5jlaKrjcG5NR5clNABfFFyhgc0jBikyS4abAG8jc2efeaTrFuQwdoF4sE8YiVrkiVj2X5Xoi6sBk=
matrix:
- TOX_SUFFIX="flakes"
- TOX_SUFFIX="requests27"
- TOX_SUFFIX="requests"
- TOX_SUFFIX="httplib2"
- TOX_SUFFIX="boto3"
- TOX_SUFFIX="urllib3121"
- TOX_SUFFIX="urllib3"
- TOX_SUFFIX="tornado4"
- TOX_SUFFIX="aiohttp"
matrix:
@@ -18,35 +17,26 @@ matrix:
- env: TOX_SUFFIX="flakes"
python: 3.7
dist: xenial
sudo: true
- env: TOX_SUFFIX="requests27"
- env: TOX_SUFFIX="requests"
python: 3.7
dist: xenial
sudo: true
- env: TOX_SUFFIX="httplib2"
python: 3.7
dist: xenial
sudo: true
- env: TOX_SUFFIX="urllib3121"
- env: TOX_SUFFIX="urllib3"
python: 3.7
dist: xenial
sudo: true
- env: TOX_SUFFIX="tornado4"
python: 3.7
dist: xenial
sudo: true
- env: TOX_SUFFIX="aiohttp"
python: 3.7
dist: xenial
sudo: true
allow_failures:
- env: TOX_SUFFIX="boto3"
- env: TOX_SUFFIX="aiohttp"
python: "pypy3.5-5.9.0"
exclude:
# Only run flakes on a single Python 2.x and a single 3.x
- env: TOX_SUFFIX="flakes"
python: 3.4
- env: TOX_SUFFIX="flakes"
python: 3.5
- env: TOX_SUFFIX="flakes"
@@ -64,7 +54,9 @@ python:
- pypy
- "pypy3.5-5.9.0"
install:
- pip install tox-travis
- pip install tox-travis codecov
- if [[ $TOX_SUFFIX != 'flakes' ]]; then python setup.py install ; fi
script:
- tox -e "${TOX_SUFFIX}"
after_success:
- codecov

View File

@@ -1,4 +1,4 @@
|PyPI| |Python versions| |Build Status| |Waffle Ready| |Gitter|
|PyPI| |Python versions| |Build Status| |CodeCov| |Gitter|
VCR.py
======
@@ -53,8 +53,10 @@ more details
:target: https://pypi.python.org/pypi/vcrpy
.. |Build Status| image:: https://secure.travis-ci.org/kevin1024/vcrpy.svg?branch=master
:target: http://travis-ci.org/kevin1024/vcrpy
.. |Waffle Ready| image:: https://badge.waffle.io/kevin1024/vcrpy.svg?label=ready&title=waffle
:target: https://waffle.io/kevin1024/vcrpy
.. |Gitter| image:: https://badges.gitter.im/Join%20Chat.svg
:alt: Join the chat at https://gitter.im/kevin1024/vcrpy
:target: https://gitter.im/kevin1024/vcrpy?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge
.. |CodeCov| image:: https://codecov.io/gh/kevin1024/vcrpy/branch/master/graph/badge.svg
:target: https://codecov.io/gh/kevin1024/vcrpy
:alt: Code Coverage Status

View File

@@ -97,8 +97,12 @@ Create your own method with the following signature
def my_matcher(r1, r2):
Your method receives the two requests and must return ``True`` if they
match, ``False`` if they don't.
Your method receives the two requests and can return :
- Use an ``assert`` statement in the matcher, then we have ``None`` if they match, raise an `AssertionError`` if they don't.
- A boolean, ``True`` if they match, ``False`` if they don't.
Note : You should use an ``assert`` statement in order to have feedback when a matcher is failing.
Finally, register your method with VCR to use your new request matcher.
@@ -107,7 +111,7 @@ Finally, register your method with VCR to use your new request matcher.
import vcr
def jurassic_matcher(r1, r2):
return r1.uri == r2.uri and 'JURASSIC PARK' in r1.body
assert r1.uri == r2.uri and 'JURASSIC PARK' in r1.body
my_vcr = vcr.VCR()
my_vcr.register_matcher('jurassic', jurassic_matcher)
@@ -221,24 +225,25 @@ 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_request`` configuration option to so this.
Here is an example that will never record requests to the /login
endpoint.
callback with the ``before_record_request`` configuration option to
manipulate the HTTP request before adding it to the cassette, or return
``None`` to ignore it entirely. Here is an example that will never record
requests to the ``'/login'`` path:
.. code:: python
def before_record_cb(request):
if request.path != '/login':
return request
if request.path == '/login':
return None
return request
my_vcr = vcr.VCR(
before_record_request = before_record_cb,
before_record_request=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
You can also mutate the request using this callback. For example, you
could remove all query parameters from any requests to the ``'/login'``
path.
@@ -246,7 +251,7 @@ path.
def scrub_login_request(request):
if request.path == '/login':
request.uri, _ = urllib.splitquery(response.uri)
request.uri, _ = urllib.splitquery(request.uri)
return request
my_vcr = vcr.VCR(
@@ -258,9 +263,12 @@ path.
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``:
You can also do response filtering with the
``before_record_response`` configuration option. Its usage is
similar to the above ``before_record_request`` - you can
mutate the response, or return ``None`` to avoid recording
the request and response altogether. For example to hide
sensitive data from the request body:
.. code:: python
@@ -302,8 +310,8 @@ in a few ways:
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
- Add a ``before_record_request`` or ``before_record_response`` callback
that returns ``None`` for requests you want to ignore (see above).
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
@@ -364,3 +372,16 @@ cassette names, use ``VCR.ensure_suffix`` as follows:
@my_vcr.use_cassette
def my_test_function():
Rewind Cassette
---------------
VCR.py allows to rewind a cassette in order to replay it inside the same function/test.
.. code:: python
with vcr.use_cassette('fixtures/vcr_cassettes/synopsis.yaml') as cass:
response = urllib2.urlopen('http://www.zombo.com/').read()
assert cass.all_played
a.rewind()
assert not cass.all_played

View File

@@ -1,5 +1,18 @@
Changelog
---------
- 2.1.1 (UNRELEASED)
- 2.1.0 - Add a `rewind` method to reset a cassette (thanks @khamidou)
New error message with more details on why the cassette failed to play a request (thanks @arthurHamon2, @neozenith)
Handle connect tunnel URI (thanks @jeking3)
Add code coverage to the project (thanks @neozenith)
Drop support to python 3.4
Add deprecation warning on python 2.7, next major release will drop python 2.7 support
Fix build problems on requests tests (thanks to @dunossauro)
Fix matching on 'body' failing when Unicode symbols are present in them (thanks @valgur)
Fix bugs on aiohttp integration (thanks @graingert, @steinnes, @stj, @lamenezes, @lmazuel)
Fix Biopython incompatibility (thanks @rishab121)
Fix Boto3 integration (thanks @1oglop1, @arthurHamon2)
- 2.0.1 - Fix bug when using vcrpy with python 3.4
- 2.0.0 - Support python 3.7 (fix httplib2 and urllib2, thanks @felixonmars)
[#356] Fixes `before_record_response` so the original response isn't changed (thanks @kgraves)
Fix requests stub when using proxy (thanks @samuelfekete @daneoshiga)

View File

@@ -11,7 +11,10 @@ yourself using `py.test <http://pytest.org/>`__ and
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'"
tox -e {pyNN}-{HTTP_LIBRARY} -- <pytest flags passed through>
tox -e py27-requests -- -v -k "'test_status_code or test_gzip'"
tox -e py37-requests -- -v --last-failed
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
@@ -23,3 +26,24 @@ documentation <https://boto.readthedocs.io/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.
Troubleshooting on MacOSX
-------------------------
If you have this kind of error when running tox :
.. code:: python
__main__.ConfigurationError: Curl is configured to use SSL, but we have
not been able to determine which SSL backend it is using. Please see PycURL documentation for how to specify the SSL backend manually.
Then you need to define some environment variables:
.. code:: bash
export PYCURL_SSL_LIBRARY=openssl
export LDFLAGS=-L/usr/local/opt/openssl/lib
export CPPFLAGS=-I/usr/local/opt/openssl/include
Reference : `stackoverflow issue <https://stackoverflow.com/questions/51019622/curl-is-configured-to-use-ssl-but-we-have-not-been-able-to-determine-which-ssl>`__

View File

@@ -29,3 +29,29 @@ The second time, you will see::
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.
CannotOverwriteExistingCassetteException
========================================
When a request failed to be found in an existing cassette,
VCR.py tries to get the request(s) that may be similar to the one being searched.
The goal is to see which matcher(s) failed and understand what part of the failed request may have changed.
It can return multiple similar requests with :
- the matchers that have succeeded
- the matchers that have failed
- for each failed matchers, why it has failed with an assertion message
CannotOverwriteExistingCassetteException message example :
.. code:: python
CannotOverwriteExistingCassetteException: Can't overwrite existing cassette ('cassette.yaml') in your current record mode ('once').
No match for the request (<Request (GET) https://www.googleapis.com/?alt=json&maxResults=200>) was found.
Found 1 similar requests with 1 different matchers :
1 - (<Request (GET) https://www.googleapis.com/?alt=json&maxResults=500>).
Matchers succeeded : ['method', 'scheme', 'host', 'port', 'path']
Matchers failed :
query - assertion failure :
[('alt', 'json'), ('maxResults', '200')] != [('alt', 'json'), ('maxResults', '500')]

View File

@@ -9,7 +9,7 @@ with pip::
Compatibility
-------------
VCR.py supports Python 2.7 and 3.4+, and
VCR.py supports Python 2.7 and 3.5+, and
`pypy <http://pypy.org>`__.
The following HTTP libraries are supported:

View File

@@ -11,7 +11,7 @@ Usage
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
``fixtures/vcr_cassettes/synopsis.yaml``. 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
@@ -95,3 +95,9 @@ Unittest Integration
While it's possible to use the context manager or decorator forms with unittest,
there's also a ``VCRTestCase`` provided separately by `vcrpy-unittest
<https://github.com/agriffis/vcrpy-unittest>`__.
Pytest Integration
------------------
A Pytest plugin is available here : `pytest-vcr
<https://github.com/ktosiek/pytest-vcr>`__.

View File

@@ -1,3 +1,7 @@
#!/bin/bash
# https://blog.ionelmc.ro/2015/04/14/tox-tricks-and-patterns/#when-it-inevitably-leads-to-shell-scripts
# If you are getting an INVOCATION ERROR for this script then there is
# a good chance you are running on Windows.
# You can and should use WSL for running tox on Windows when it calls bash scripts.
REQUESTS_CA_BUNDLE=`python -m pytest_httpbin.certs` py.test $*

View File

@@ -28,7 +28,7 @@ install_requires = [
'six>=1.5',
'contextlib2; python_version=="2.7"',
'mock; python_version=="2.7"',
'yarl; python_version>="3.4"',
'yarl; python_version>="3.5"',
]
excluded_packages = ["tests*"]
@@ -37,7 +37,7 @@ if sys.version_info[0] == 2:
setup(
name='vcrpy',
version='2.0.0',
version='2.1.0',
description=(
"Automatically mock your HTTP interactions to simplify and "
"speed up testing"
@@ -47,19 +47,18 @@ setup(
author_email='me@kevinmccarthy.org',
url='https://github.com/kevin1024/vcrpy',
packages=find_packages(exclude=excluded_packages),
python_requires='>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*',
python_requires='>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*',
install_requires=install_requires,
license='MIT',
tests_require=['pytest', 'mock', 'pytest-httpbin'],
classifiers=[
'Development Status :: 4 - Beta',
'Development Status :: 5 - Production/Stable',
'Environment :: Console',
'Intended Audience :: Developers',
'Programming Language :: Python',
'Programming Language :: Python :: 2',
'Programming Language :: Python :: 2.7',
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.4',
'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: 3.6',
'Programming Language :: Python :: 3.7',

View File

@@ -9,7 +9,7 @@ interactions:
method: GET
uri: http://httpbin.org/ip
response:
body: {string: !!python/unicode "{\n \"origin\": \"217.122.164.194\"\n}"}
body: {string: "{\n \"origin\": \"217.122.164.194\"\n}"}
headers:
access-control-allow-origin: ['*']
content-type: [application/json]

View File

@@ -17,6 +17,8 @@ async def aiohttp_request(loop, method, url, output='text', encoding='utf-8', co
content = await response.json(encoding=encoding, content_type=content_type)
elif output == 'raw':
content = await response.read()
elif output == 'stream':
content = await response.content.read()
response_ctx._resp.close()
await session.close()
@@ -28,6 +30,14 @@ def aiohttp_app():
async def hello(request):
return aiohttp.web.Response(text='hello')
async def json(request):
return aiohttp.web.json_response({})
async def json_empty_body(request):
return aiohttp.web.json_response()
app = aiohttp.web.Application()
app.router.add_get('/', hello)
app.router.add_get('/json', json)
app.router.add_get('/json/empty', json_empty_body)
return app

View File

@@ -1,4 +1,5 @@
import contextlib
import logging
import pytest
asyncio = pytest.importorskip("asyncio")
@@ -47,14 +48,32 @@ def test_status(tmpdir, scheme):
assert cassette.play_count == 1
def test_headers(tmpdir, scheme):
@pytest.mark.parametrize("auth", [None, aiohttp.BasicAuth("vcrpy", "test")])
def test_headers(tmpdir, scheme, auth):
url = scheme + '://httpbin.org'
with vcr.use_cassette(str(tmpdir.join('headers.yaml'))):
response, _ = get(url)
response, _ = get(url, auth=auth)
with vcr.use_cassette(str(tmpdir.join('headers.yaml'))) as cassette:
if auth is not None:
request = cassette.requests[0]
assert "AUTHORIZATION" in request.headers
cassette_response, _ = get(url, auth=auth)
assert dict(cassette_response.headers) == dict(response.headers)
assert cassette.play_count == 1
assert 'istr' not in cassette.data[0]
assert 'yarl.URL' not in cassette.data[0]
def test_case_insensitive_headers(tmpdir, scheme):
url = scheme + '://httpbin.org'
with vcr.use_cassette(str(tmpdir.join('whatever.yaml'))):
_, _ = get(url)
with vcr.use_cassette(str(tmpdir.join('whatever.yaml'))) as cassette:
cassette_response, _ = get(url)
assert cassette_response.headers == response.headers
assert "Content-Type" in cassette_response.headers
assert "content-type" in cassette_response.headers
assert cassette.play_count == 1
@@ -93,17 +112,43 @@ def test_binary(tmpdir, scheme):
assert cassette.play_count == 1
def test_post(tmpdir, scheme):
def test_stream(tmpdir, scheme):
url = scheme + '://httpbin.org/get'
with vcr.use_cassette(str(tmpdir.join('stream.yaml'))):
resp, body = get(url, output='raw') # Do not use stream here, as the stream is exhausted by vcr
with vcr.use_cassette(str(tmpdir.join('stream.yaml'))) as cassette:
cassette_resp, cassette_body = get(url, output='stream')
assert cassette_body == body
assert cassette.play_count == 1
@pytest.mark.parametrize('body', ['data', 'json'])
def test_post(tmpdir, scheme, body, caplog):
caplog.set_level(logging.INFO)
data = {'key1': 'value1', 'key2': 'value2'}
url = scheme + '://httpbin.org/post'
with vcr.use_cassette(str(tmpdir.join('post.yaml'))):
_, response_json = post(url, data=data)
_, response_json = post(url, **{body: data})
with vcr.use_cassette(str(tmpdir.join('post.yaml'))) as cassette:
_, cassette_response_json = post(url, data=data)
request = cassette.requests[0]
assert request.body == data
_, cassette_response_json = post(url, **{body: data})
assert cassette_response_json == response_json
assert cassette.play_count == 1
assert next(
(
log
for log in caplog.records
if log.getMessage()
== '<Request (POST) {}> not in cassette, sending to real server'.format(url)
),
None,
), 'Log message not found.'
def test_params(tmpdir, scheme):
url = scheme + '://httpbin.org/get'
@@ -168,6 +213,8 @@ def test_aiohttp_test_client(aiohttp_client, tmpdir):
assert response.status == 200
response_text = loop.run_until_complete(response.text())
assert response_text == 'hello'
response_text = loop.run_until_complete(response.text(errors='replace'))
assert response_text == 'hello'
with vcr.use_cassette(str(tmpdir.join('get.yaml'))) as cassette:
response = loop.run_until_complete(client.get(url))
@@ -177,3 +224,41 @@ def test_aiohttp_test_client(aiohttp_client, tmpdir):
response_text = loop.run_until_complete(response.text())
assert response_text == 'hello'
assert cassette.play_count == 1
def test_aiohttp_test_client_json(aiohttp_client, tmpdir):
loop = asyncio.get_event_loop()
app = aiohttp_app()
url = '/json/empty'
client = loop.run_until_complete(aiohttp_client(app))
with vcr.use_cassette(str(tmpdir.join('get.yaml'))):
response = loop.run_until_complete(client.get(url))
assert response.status == 200
response_json = loop.run_until_complete(response.json())
assert response_json is None
with vcr.use_cassette(str(tmpdir.join('get.yaml'))) as cassette:
response = loop.run_until_complete(client.get(url))
request = cassette.requests[0]
assert request.url == str(client.make_url(url))
response_json = loop.run_until_complete(response.json())
assert response_json is None
assert cassette.play_count == 1
def test_redirect(aiohttp_client, tmpdir):
url = 'https://httpbin.org/redirect/2'
with vcr.use_cassette(str(tmpdir.join('redirect.yaml'))):
response, _ = get(url)
with vcr.use_cassette(str(tmpdir.join('redirect.yaml'))) as cassette:
cassette_response, _ = get(url)
assert cassette_response.status == response.status
assert len(cassette_response.history) == len(response.history)
assert len(cassette) == 3
assert cassette.play_count == 3

View File

@@ -1,15 +1,60 @@
import pytest
import os
boto3 = pytest.importorskip("boto3")
import boto3 # NOQA
import botocore # NOQA
import vcr # NOQA
bucket = 'boto3-demo-1337' # a bucket you can access
key = 'test/my_test.txt' # key with r+w access
content = 'hello world i am a string' # content to put in the test file
try:
from botocore import awsrequest # NOQA
botocore_awsrequest = True
except ImportError:
botocore_awsrequest = False
# skip tests if boto does not use vendored requests anymore
# https://github.com/boto/botocore/pull/1495
boto3_skip_vendored_requests = pytest.mark.skipif(
botocore_awsrequest,
reason='botocore version {ver} does not use vendored requests anymore.'.format(
ver=botocore.__version__))
boto3_skip_awsrequest = pytest.mark.skipif(
not botocore_awsrequest,
reason='botocore version {ver} still uses vendored requests.'.format(
ver=botocore.__version__))
IAM_USER_NAME = "vcrpy"
def test_boto_stubs(tmpdir):
@pytest.fixture
def iam_client():
def _iam_client(boto3_session=None):
if boto3_session is None:
boto3_session = boto3.Session(
aws_access_key_id=os.environ.get('AWS_ACCESS_KEY_ID', "default"),
aws_secret_access_key=os.environ.get('AWS_SECRET_ACCESS_KEY', "default"),
aws_session_token=None,
region_name=os.environ.get('AWS_DEFAULT_REGION', "default"),
)
return boto3_session.client('iam')
return _iam_client
@pytest.fixture
def get_user(iam_client):
def _get_user(client=None, user_name=IAM_USER_NAME):
if client is None:
# Default client set with fixture `iam_client`
client = iam_client()
return client.get_user(UserName=user_name)
return _get_user
@boto3_skip_vendored_requests
def test_boto_vendored_stubs(tmpdir):
with vcr.use_cassette(str(tmpdir.join('boto3-stubs.yml'))):
# Perform the imports within the patched context so that
# HTTPConnection, VerifiedHTTPSConnection refers to the patched version.
@@ -23,45 +68,50 @@ def test_boto_stubs(tmpdir):
VerifiedHTTPSConnection('hostname.does.not.matter')
def test_boto3_without_vcr():
s3_resource = boto3.resource('s3')
b = s3_resource.Bucket(bucket)
b.put_object(Key=key, Body=content)
@pytest.mark.skipif(
os.environ.get("TRAVIS_PULL_REQUEST") != "false",
reason="Encrypted Environment Variables from Travis Repository Settings"
" are disabled on PRs from forks. "
"https://docs.travis-ci.com/user/pull-requests/#pull-requests-and-security-restrictions"
)
def test_boto_medium_difficulty(tmpdir, get_user):
# retrieve content to check it
o = s3_resource.Object(bucket, key).get()
# decode for python3
assert content == o['Body'].read().decode('utf-8')
def test_boto_medium_difficulty(tmpdir):
s3_resource = boto3.resource('s3')
b = s3_resource.Bucket(bucket)
with vcr.use_cassette(str(tmpdir.join('boto3-medium.yml'))):
b.put_object(Key=key, Body=content)
o = s3_resource.Object(bucket, key).get()
assert content == o['Body'].read().decode('utf-8')
response = get_user()
assert response['User']['UserName'] == IAM_USER_NAME
with vcr.use_cassette(str(tmpdir.join('boto3-medium.yml'))) as cass:
b.put_object(Key=key, Body=content)
o = s3_resource.Object(bucket, key).get()
assert content == o['Body'].read().decode('utf-8')
response = get_user()
assert response['User']['UserName'] == IAM_USER_NAME
assert cass.all_played
def test_boto_hardcore_mode(tmpdir):
@pytest.mark.skipif(
os.environ.get("TRAVIS_PULL_REQUEST") != "false",
reason="Encrypted Environment Variables from Travis Repository Settings"
" are disabled on PRs from forks. "
"https://docs.travis-ci.com/user/pull-requests/#pull-requests-and-security-restrictions"
)
def test_boto_hardcore_mode(tmpdir, iam_client, get_user):
with vcr.use_cassette(str(tmpdir.join('boto3-hardcore.yml'))):
s3_resource = boto3.resource('s3')
b = s3_resource.Bucket(bucket)
b.put_object(Key=key, Body=content)
o = s3_resource.Object(bucket, key).get()
assert content == o['Body'].read().decode('utf-8')
ses = boto3.Session(
aws_access_key_id=os.environ.get('AWS_ACCESS_KEY_ID'),
aws_secret_access_key=os.environ.get('AWS_SECRET_ACCESS_KEY'),
region_name=os.environ.get('AWS_DEFAULT_REGION'),
)
client = iam_client(ses)
response = get_user(client=client)
assert response['User']['UserName'] == IAM_USER_NAME
with vcr.use_cassette(str(tmpdir.join('boto3-hardcore.yml'))) as cass:
s3_resource = boto3.resource('s3')
b = s3_resource.Bucket(bucket)
b.put_object(Key=key, Body=content)
o = s3_resource.Object(bucket, key).get()
assert content == o['Body'].read().decode('utf-8')
ses = boto3.Session(
aws_access_key_id=os.environ.get('AWS_ACCESS_KEY_ID'),
aws_secret_access_key=os.environ.get('AWS_SECRET_ACCESS_KEY'),
aws_session_token=None,
region_name=os.environ.get('AWS_DEFAULT_REGION'),
)
client = iam_client(ses)
response = get_user(client=client)
assert response['User']['UserName'] == IAM_USER_NAME
assert cass.all_played

View File

@@ -22,7 +22,7 @@ def http():
kwargs = {
'ca_certs': pytest_httpbin.certs.where()
}
if sys.version_info[:2] == (3, 7):
if sys.version_info[:2] in [(2, 7), (3, 7)]:
kwargs['disable_ssl_certificate_validation'] = True
return httplib2.Http(**kwargs)

View File

@@ -44,7 +44,7 @@ def proxy_server():
target=httpd.serve_forever,
)
proxy_process.start()
yield 'http://{0}:{1}'.format(*httpd.server_address)
yield 'http://{}:{}'.format(*httpd.server_address)
proxy_process.terminate()

View File

@@ -116,10 +116,10 @@ def test_post_chunked_binary(tmpdir, httpbin):
assert req1 == req2
@pytest.mark.xskip('sys.version_info >= (3, 6)', strict=True, raises=ConnectionError)
@pytest.mark.xskip((3, 5) < sys.version_info < (3, 6) and
platform.python_implementation() == 'CPython',
reason='Fails on CPython 3.5')
@pytest.mark.skipif('sys.version_info >= (3, 6)', strict=True, raises=ConnectionError)
@pytest.mark.skipif((3, 5) < sys.version_info < (3, 6) and
platform.python_implementation() == 'CPython',
reason='Fails on CPython 3.5')
def test_post_chunked_binary_secure(tmpdir, httpbin_secure):
'''Ensure that we can send chunked binary without breaking while trying to concatenate bytes with str.'''
data1 = iter([b'data', b'to', b'send'])
@@ -254,7 +254,7 @@ def test_nested_cassettes_with_session_created_before_nesting(httpbin_both, tmpd
def test_post_file(tmpdir, httpbin_both):
'''Ensure that we handle posting a file.'''
url = httpbin_both + '/post'
with vcr.use_cassette(str(tmpdir.join('post_file.yaml'))) as cass, open('tox.ini') as f:
with vcr.use_cassette(str(tmpdir.join('post_file.yaml'))) as cass, open('tox.ini', 'rb') 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.
@@ -282,3 +282,17 @@ def test_filter_post_params(tmpdir, httpbin_both):
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
def test_post_unicode_match_on_body(tmpdir, httpbin_both):
'''Ensure that matching on POST body that contains Unicode characters works.'''
data = {'key1': 'value1', '●‿●': '٩(●̮̮̃•̃)۶'}
url = httpbin_both + '/post'
with vcr.use_cassette(str(tmpdir.join('requests.yaml')), additional_matchers=('body',)):
req1 = requests.post(url, data).content
with vcr.use_cassette(str(tmpdir.join('requests.yaml')), additional_matchers=('body',)):
req2 = requests.post(url, data).content
assert req1 == req2

View File

@@ -20,7 +20,7 @@ def verify_pool_mgr():
@pytest.fixture(scope='module')
def pool_mgr():
return urllib3.PoolManager()
return urllib3.PoolManager(cert_reqs='CERT_NONE')
def test_status_code(httpbin_both, tmpdir, verify_pool_mgr):

View File

@@ -59,7 +59,7 @@ def test_flickr_multipart_upload(httpbin, tmpdir):
def test_flickr_should_respond_with_200(tmpdir):
testfile = str(tmpdir.join('flickr.yml'))
with vcr.use_cassette(testfile):
r = requests.post("http://api.flickr.com/services/upload")
r = requests.post("https://api.flickr.com/services/upload", verify=False)
assert r.status_code == 200
@@ -81,16 +81,23 @@ def test_amazon_doctype(tmpdir):
assert 'html' in r.text
def start_rpc_server(q):
httpd = xmlrpc_server.SimpleXMLRPCServer(('127.0.0.1', 0))
httpd.register_function(pow)
q.put('http://{}:{}'.format(*httpd.server_address))
httpd.serve_forever()
@pytest.yield_fixture(scope='session')
def rpc_server():
httpd = xmlrpc_server.SimpleXMLRPCServer(('', 0))
httpd.register_function(pow)
q = multiprocessing.Queue()
proxy_process = multiprocessing.Process(
target=httpd.serve_forever,
target=start_rpc_server,
args=(q,)
)
try:
proxy_process.start()
yield 'http://{}:{}'.format(*httpd.server_address)
yield q.get()
finally:
proxy_process.terminate()

View File

@@ -133,6 +133,17 @@ def test_cassette_all_played():
assert a.all_played
@mock.patch('vcr.cassette.requests_match', _mock_requests_match)
def test_cassette_rewound():
a = Cassette('test')
a.append('foo', 'bar')
a.play_response('foo')
assert a.all_played
a.rewind()
assert not 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)
@@ -306,3 +317,51 @@ def test_use_as_decorator_on_generator():
yield 2
assert list(test_function()) == [1, 2]
@mock.patch("vcr.cassette.get_matchers_results")
def test_find_requests_with_most_matches_one_similar_request(mock_get_matchers_results):
mock_get_matchers_results.side_effect = [
(["method"], [("path", "failed : path"), ("query", "failed : query")]),
(["method", "path"], [("query", "failed : query")]),
([], [("method", "failed : method"), ("path", "failed : path"), ("query", "failed : query")]),
]
cassette = Cassette("test")
for request in range(1, 4):
cassette.append(request, 'response')
result = cassette.find_requests_with_most_matches("fake request")
assert result == [(2, ["method", "path"], [("query", "failed : query")])]
@mock.patch("vcr.cassette.get_matchers_results")
def test_find_requests_with_most_matches_no_similar_requests(mock_get_matchers_results):
mock_get_matchers_results.side_effect = [
([], [("path", "failed : path"), ("query", "failed : query")]),
([], [("path", "failed : path"), ("query", "failed : query")]),
([], [("path", "failed : path"), ("query", "failed : query")]),
]
cassette = Cassette("test")
for request in range(1, 4):
cassette.append(request, 'response')
result = cassette.find_requests_with_most_matches("fake request")
assert result == []
@mock.patch("vcr.cassette.get_matchers_results")
def test_find_requests_with_most_matches_many_similar_requests(mock_get_matchers_results):
mock_get_matchers_results.side_effect = [
(["method", "path"], [("query", "failed : query")]),
(["method"], [("path", "failed : path"), ("query", "failed : query")]),
(["method", "path"], [("query", "failed : query")]),
]
cassette = Cassette("test")
for request in range(1, 4):
cassette.append(request, 'response')
result = cassette.find_requests_with_most_matches("fake request")
assert result == [
(1, ["method", "path"], [("query", "failed : query")]),
(3, ["method", "path"], [("query", "failed : query")])
]

73
tests/unit/test_errors.py Normal file
View File

@@ -0,0 +1,73 @@
import pytest
from vcr.compat import mock
from vcr import errors
from vcr.cassette import Cassette
@mock.patch("vcr.cassette.Cassette.find_requests_with_most_matches")
@pytest.mark.parametrize(
"most_matches, expected_message",
[
# No request match found
(
[],
"No similar requests, that have not been played, found."
),
# One matcher failed
(
[("similar request", ["method", "path"], [("query", "failed : query")])],
"Found 1 similar requests with 1 different matcher(s) :\n"
"\n1 - ('similar request').\n"
"Matchers succeeded : ['method', 'path']\n"
"Matchers failed :\n"
"query - assertion failure :\n"
"failed : query\n"
),
# Multiple failed matchers
(
[("similar request", ["method"], [("query", "failed : query"), ("path", "failed : path")])],
"Found 1 similar requests with 2 different matcher(s) :\n"
"\n1 - ('similar request').\n"
"Matchers succeeded : ['method']\n"
"Matchers failed :\n"
"query - assertion failure :\n"
"failed : query\n"
"path - assertion failure :\n"
"failed : path\n"
),
# Multiple similar requests
(
[
("similar request", ["method"], [("query", "failed : query")]),
("similar request 2", ["method"], [("query", "failed : query 2")])
],
"Found 2 similar requests with 1 different matcher(s) :\n"
"\n1 - ('similar request').\n"
"Matchers succeeded : ['method']\n"
"Matchers failed :\n"
"query - assertion failure :\n"
"failed : query\n"
"\n2 - ('similar request 2').\n"
"Matchers succeeded : ['method']\n"
"Matchers failed :\n"
"query - assertion failure :\n"
"failed : query 2\n"
),
]
)
def test_CannotOverwriteExistingCassetteException_get_message(
mock_find_requests_with_most_matches, most_matches, expected_message):
mock_find_requests_with_most_matches.return_value = most_matches
cassette = Cassette("path")
failed_request = "request"
exception_message = errors.CannotOverwriteExistingCassetteException._get_message(
cassette, "request"
)
expected = "Can't overwrite existing cassette (%r) in your current record mode (%r).\n" \
"No match for the request (%r) was found.\n" \
"%s" % (cassette._path, cassette.record_mode, failed_request, expected_message)
assert exception_message == expected

View File

@@ -1,4 +1,5 @@
import itertools
from vcr.compat import mock
import pytest
@@ -21,20 +22,22 @@ REQUESTS = {
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 {k1, k2}:
assert not matched
expecting_assertion_error = matcher_name in {k1, k2}
if expecting_assertion_error:
with pytest.raises(AssertionError):
matcher(REQUESTS[k1], REQUESTS[k2])
else:
assert matched
assert matcher(REQUESTS[k1], REQUESTS[k2]) is None
def test_uri_matcher():
for k1, k2 in itertools.permutations(REQUESTS, 2):
matched = matchers.uri(REQUESTS[k1], REQUESTS[k2])
if {k1, k2} != {'base', 'method'}:
assert not matched
expecting_assertion_error = {k1, k2} != {"base", "method"}
if expecting_assertion_error:
with pytest.raises(AssertionError):
matchers.uri(REQUESTS[k1], REQUESTS[k2])
else:
assert matched
assert matchers.uri(REQUESTS[k1], REQUESTS[k2]) is None
req1_body = (b"<?xml version='1.0'?><methodCall><methodName>test</methodName>"
@@ -107,7 +110,7 @@ req2_body = (b"<?xml version='1.0'?><methodCall><methodName>test</methodName>"
)
])
def test_body_matcher_does_match(r1, r2):
assert matchers.body(r1, r2)
assert matchers.body(r1, r2) is None
@pytest.mark.parametrize("r1, r2", [
@@ -135,25 +138,128 @@ def test_body_matcher_does_match(r1, r2):
)
])
def test_body_match_does_not_match(r1, r2):
assert not matchers.body(r1, r2)
with pytest.raises(AssertionError):
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&c=d", "", {})
req2 = request.Request("GET", "http://host.com/?c=d&a=b", "", {})
assert matchers.query(req1, req2) is None
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)
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) is None
assert matchers.query(req1, req3) is None
def test_metchers():
assert_matcher('method')
assert_matcher('scheme')
assert_matcher('host')
assert_matcher('port')
assert_matcher('path')
assert_matcher('query')
def test_matchers():
assert_matcher("method")
assert_matcher("scheme")
assert_matcher("host")
assert_matcher("port")
assert_matcher("path")
assert_matcher("query")
def test_evaluate_matcher_does_match():
def bool_matcher(r1, r2):
return True
def assertion_matcher(r1, r2):
assert 1 == 1
r1, r2 = None, None
for matcher in [bool_matcher, assertion_matcher]:
match, assertion_msg = matchers._evaluate_matcher(matcher, r1, r2)
assert match is True
assert assertion_msg is None
def test_evaluate_matcher_does_not_match():
def bool_matcher(r1, r2):
return False
def assertion_matcher(r1, r2):
# This is like the "assert" statement preventing pytest to recompile it
raise AssertionError()
r1, r2 = None, None
for matcher in [bool_matcher, assertion_matcher]:
match, assertion_msg = matchers._evaluate_matcher(matcher, r1, r2)
assert match is False
assert not assertion_msg
def test_evaluate_matcher_does_not_match_with_assert_message():
def assertion_matcher(r1, r2):
# This is like the "assert" statement preventing pytest to recompile it
raise AssertionError("Failing matcher")
r1, r2 = None, None
match, assertion_msg = matchers._evaluate_matcher(assertion_matcher, r1, r2)
assert match is False
assert assertion_msg == "Failing matcher"
def test_get_assertion_message():
assert matchers.get_assertion_message(None) is None
assert matchers.get_assertion_message("") == ""
def test_get_assertion_message_with_details():
assertion_msg = "q1=1 != q2=1"
expected = assertion_msg
assert matchers.get_assertion_message(assertion_msg) == expected
@pytest.mark.parametrize(
"r1, r2, expected_successes, expected_failures",
[
(
request.Request("GET", "http://host.com/p?a=b", "", {}),
request.Request("GET", "http://host.com/p?a=b", "", {}),
["method", "path"],
[],
),
(
request.Request("GET", "http://host.com/p?a=b", "", {}),
request.Request("POST", "http://host.com/p?a=b", "", {}),
["path"],
["method"],
),
(
request.Request("GET", "http://host.com/p?a=b", "", {}),
request.Request("POST", "http://host.com/path?a=b", "", {}),
[],
["method", "path"],
),
],
)
def test_get_matchers_results(r1, r2, expected_successes, expected_failures):
successes, failures = matchers.get_matchers_results(
r1, r2, [matchers.method, matchers.path]
)
assert successes == expected_successes
assert len(failures) == len(expected_failures)
for i, expected_failure in enumerate(expected_failures):
assert failures[i][0] == expected_failure
assert failures[i][1] is not None
@mock.patch("vcr.matchers.get_matchers_results")
@pytest.mark.parametrize(
"successes, failures, expected_match",
[
(["method", "path"], [], True),
(["method"], ["path"], False),
([], ["method", "path"], False),
],
)
def test_requests_match(mock_get_matchers_results, successes, failures, expected_match):
mock_get_matchers_results.return_value = (successes, failures)
r1 = request.Request("GET", "http://host.com/p?a=b", "", {})
r2 = request.Request("GET", "http://host.com/p?a=b", "", {})
match = matchers.requests_match(r1, r2, [matchers.method, matchers.path])
assert match is expected_match

View File

@@ -5,6 +5,12 @@ import yaml
import vcr.migration
# Use the libYAML versions if possible
try:
from yaml import CLoader as Loader
except ImportError:
from yaml import Loader
def test_try_migrate_with_json(tmpdir):
cassette = tmpdir.join('cassette.json').strpath
@@ -22,9 +28,9 @@ def test_try_migrate_with_yaml(tmpdir):
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)
expected_yaml = yaml.load(f, Loader=Loader)
with open(cassette, 'r') as f:
actual_yaml = yaml.load(f)
actual_yaml = yaml.load(f, Loader=Loader)
assert actual_yaml == expected_yaml

View File

@@ -3,9 +3,13 @@ import pytest
from vcr.request import Request, HeadersDict
def test_str():
req = Request('GET', 'http://www.google.com/', '', {})
str(req) == '<Request (GET) http://www.google.com/>'
@pytest.mark.parametrize("method, uri, expected_str", [
('GET', 'http://www.google.com/', '<Request (GET) http://www.google.com/>'),
('OPTIONS', '*', '<Request (OPTIONS) *>'),
('CONNECT', 'host.some.where:1234', '<Request (CONNECT) host.some.where:1234>')
])
def test_str(method, uri, expected_str):
assert str(Request(method, uri, '', {})) == expected_str
def test_headers():
@@ -29,18 +33,21 @@ def test_add_header_deprecated():
('https://go.com/', 443),
('https://go.com:443/', 443),
('https://go.com:3000/', 3000),
('*', None)
])
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/'
@pytest.mark.parametrize("method, uri", [
('GET', 'http://go.com/'),
('GET', 'http://go.com:80/'),
('CONNECT', 'localhost:1234'),
('OPTIONS', '*')
])
def test_uri(method, uri):
assert Request(method, uri, '', {}).uri == uri
def test_HeadersDict():

View File

@@ -1,4 +1,9 @@
# coding: UTF-8
import io
import unittest
import six
from vcr.stubs import VCRHTTPResponse
@@ -66,3 +71,52 @@ def test_response_headers_should_have_correct_values():
assert response.headers.get('content-length') == "10806"
assert response.headers.get('date') == "Fri, 24 Oct 2014 18:35:37 GMT"
@unittest.skipIf(six.PY2, "Regression test for Python3 only")
def test_response_parses_correctly_and_fp_attribute_error_is_not_thrown():
"""
Regression test for https://github.com/kevin1024/vcrpy/issues/440
:return:
"""
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"\nPMID- 19416910\nOWN - NLM\nSTAT- MEDLINE\nDA - 20090513\nDCOM- "
b"20090622\nLR - "
b"20141209\nIS - 1091-6490 (Electronic)\nIS - 0027-8424 (Linking)\nVI - "
b"106\nIP - "
b"19\nDP - 2009 May 12\nTI - Genetic dissection of histone deacetylase "
b"requirement in "
b"tumor cells.\nPG - 7751-5\nLID - 10.1073/pnas.0903139106 [doi]\nAB - "
b"Histone "
b"deacetylase inhibitors (HDACi) represent a new group of drugs currently\n "
b" being "
b"tested in a wide variety of clinical applications. They are especially\n "
b" effective "
b"in preclinical models of cancer where they show antiproliferative\n "
b"action in many "
b"different types of cancer cells. Recently, the first HDACi was\n "
b"approved for the "
b"treatment of cutaneous T cell lymphomas. Most HDACi currently in\n "
b"clinical "
}
}
vcr_response = VCRHTTPResponse(recorded_response)
handle = io.TextIOWrapper(io.BufferedReader(vcr_response), encoding='utf-8')
handle = iter(handle)
articles = [line for line in handle]
assert len(articles) > 1

View File

@@ -147,7 +147,7 @@ def test_vcr_path_transformer():
# and it should still work with cassette_library_dir
vcr = VCR(cassette_library_dir='/foo')
with vcr.use_cassette('test') as cassette:
assert cassette._path == '/foo/test'
assert os.path.abspath(cassette._path) == os.path.abspath('/foo/test')
@pytest.fixture

View File

@@ -0,0 +1,16 @@
import sys
def test_vcr_import_deprecation(recwarn):
if 'vcr' in sys.modules:
# Remove imported module entry if already loaded in another test
del sys.modules['vcr']
import vcr # noqa: F401
if sys.version_info[0] == 2:
assert len(recwarn) == 1
assert issubclass(recwarn[0].category, DeprecationWarning)
else:
assert len(recwarn) == 0

39
tox.ini
View File

@@ -1,5 +1,21 @@
[tox]
envlist = {py27,py35,py36,py37,pypy}-{flakes,requests27,httplib2,urllib3121,tornado4,boto3,aiohttp}
skip_missing_interpreters=true
envlist = cov-clean,{py27,py35,py36,py37,pypy}-{flakes,requests,httplib2,urllib3,tornado4,boto3},{py35,py36,py37}-{aiohttp},cov-report
# Coverage environment tasks: cov-clean and cov-report
# https://pytest-cov.readthedocs.io/en/latest/tox.html
[testenv:cov-clean]
deps = coverage
skip_install=true
commands = coverage erase
[testenv:cov-report]
deps = coverage
skip_install=true
commands =
coverage html
coverage report --fail-under=75
[testenv:flakes]
skipsdist = True
@@ -10,24 +26,37 @@ commands =
deps = flake8
[testenv]
# Need to use develop install so that paths
# for aggregate code coverage combine
usedevelop=true
commands =
./runtests.sh {posargs}
./runtests.sh --cov=./vcr --cov-branch --cov-report=xml --cov-append {posargs}
deps =
Flask<1
Flask
mock
pytest
pytest-httpbin
pytest-cov
PyYAML
requests27: requests==2.7.0
ipaddress
requests: requests>=2.22.0
httplib2: httplib2
urllib3121: urllib3==1.21.1
urllib3: urllib3
{py27,py35,py36,pypy}-tornado4: tornado>=4,<5
{py27,py35,py36,pypy}-tornado4: pytest-tornado
{py27,py35,py36}-tornado4: pycurl
boto3: boto3
boto3: urllib3
aiohttp: aiohttp
aiohttp: pytest-asyncio
aiohttp: pytest-aiohttp
depends =
{py27,py35,py36,py37,pypy}-{flakes,requests,httplib2,urllib3,tornado4,boto3},{py35,py36,py37}-{aiohttp}: cov-clean
cov-report: {py27,py35,py36,py37,pypy}-{flakes,requests,httplib2,urllib3,tornado4,boto3},{py35,py36,py37}-{aiohttp}
passenv =
AWS_ACCESS_KEY_ID
AWS_DEFAULT_REGION
AWS_SECRET_ACCESS_KEY
[flake8]
max_line_length = 110

View File

@@ -1,4 +1,6 @@
import logging
import warnings
import sys
from .config import VCR
# Set default logging handler to avoid "No handler found" warnings.
@@ -9,6 +11,11 @@ except ImportError:
def emit(self, record):
pass
if sys.version_info[0] == 2:
warnings.warn(
"Python 2.x support of vcrpy is deprecated and will be removed in an upcoming major release.",
DeprecationWarning
)
logging.getLogger(__name__).addHandler(NullHandler())

View File

@@ -8,7 +8,7 @@ import wrapt
from .compat import contextlib
from .errors import UnhandledHTTPRequestError
from .matchers import requests_match, uri, method
from .matchers import requests_match, uri, method, get_matchers_results
from .patch import CassettePatcherBuilder
from .serializers import yamlserializer
from .persisters.filesystem import FilesystemPersister
@@ -16,11 +16,13 @@ from .util import partition_dict
try:
from asyncio import iscoroutinefunction
from ._handle_coroutine import handle_coroutine
except ImportError:
def iscoroutinefunction(*args, **kwargs):
return False
if sys.version_info[:2] >= (3, 5):
from ._handle_coroutine import handle_coroutine
else:
def handle_coroutine(*args, **kwags):
raise NotImplementedError('Not implemented on Python 2')
@@ -188,6 +190,7 @@ class Cassette(object):
self._serializer = serializer or yamlserializer
self._match_on = match_on
self._before_record_request = before_record_request or (lambda x: x)
log.info(self._before_record_request)
self._before_record_response = before_record_response or (lambda x: x)
self.inject = inject
self.record_mode = record_mode
@@ -223,6 +226,7 @@ class Cassette(object):
def append(self, request, response):
"""Add a request, response pair to this cassette"""
log.info("Appending request %s and response %s", request, response)
request = self._before_record_request(request)
if not request:
return
@@ -285,6 +289,45 @@ class Cassette(object):
% (self._path, request)
)
def rewind(self):
self.play_counts = collections.Counter()
def find_requests_with_most_matches(self, request):
"""
Get the most similar request(s) stored in the cassette
of a given request as a list of tuples like this:
- the request object
- the successful matchers as string
- the failed matchers and the related assertion message with the difference details as strings tuple
This is useful when a request failed to be found,
we can get the similar request(s) in order to know what have changed in the request parts.
"""
best_matches = []
request = self._before_record_request(request)
for index, (stored_request, response) in enumerate(self.data):
successes, fails = get_matchers_results(request, stored_request, self._match_on)
best_matches.append((len(successes), stored_request, successes, fails))
best_matches.sort(key=lambda t: t[0], reverse=True)
# Get the first best matches (multiple if equal matches)
final_best_matches = []
if not best_matches:
return final_best_matches
previous_nb_success = best_matches[0][0]
for best_match in best_matches:
nb_success = best_match[0]
# Do not keep matches that have 0 successes,
# it means that the request is totally different from
# the ones stored in the cassette
if nb_success < 1 or previous_nb_success != nb_success:
break
previous_nb_success = nb_success
final_best_matches.append(best_match[1:])
return final_best_matches
def _as_dict(self):
return {"requests": self.requests, "responses": self.responses}

View File

@@ -1,5 +1,8 @@
import copy
import collections
try:
from collections import abc as collections_abc # only works on python 3.3+
except ImportError:
import collections as collections_abc
import functools
import inspect
import os
@@ -175,7 +178,7 @@ class VCR(object):
if decode_compressed_response:
filter_functions.append(filters.decode_response)
if before_record_response:
if not isinstance(before_record_response, collections.Iterable):
if not isinstance(before_record_response, collections_abc.Iterable):
before_record_response = (before_record_response,)
filter_functions.extend(before_record_response)
@@ -241,7 +244,7 @@ class VCR(object):
filter_functions.append(self._build_ignore_hosts(hosts_to_ignore))
if before_record_request:
if not isinstance(before_record_request, collections.Iterable):
if not isinstance(before_record_request, collections_abc.Iterable):
before_record_request = (before_record_request,)
filter_functions.extend(before_record_request)

View File

@@ -1,5 +1,38 @@
class CannotOverwriteExistingCassetteException(Exception):
pass
def __init__(self, *args, **kwargs):
self.cassette = kwargs["cassette"]
self.failed_request = kwargs["failed_request"]
message = self._get_message(kwargs["cassette"], kwargs["failed_request"])
super(CannotOverwriteExistingCassetteException, self).__init__(message)
@staticmethod
def _get_message(cassette, failed_request):
"""Get the final message related to the exception"""
# Get the similar requests in the cassette that
# have match the most with the request.
best_matches = cassette.find_requests_with_most_matches(failed_request)
if best_matches:
# Build a comprehensible message to put in the exception.
best_matches_msg = "Found {} similar requests with {} different matcher(s) :\n".format(
len(best_matches), len(best_matches[0][2]))
for idx, best_match in enumerate(best_matches, start=1):
request, succeeded_matchers, failed_matchers_assertion_msgs = best_match
best_matches_msg += "\n%s - (%r).\n" \
"Matchers succeeded : %s\n" \
"Matchers failed :\n" % (idx, request, succeeded_matchers)
for failed_matcher, assertion_msg in failed_matchers_assertion_msgs:
best_matches_msg += "%s - assertion failure :\n" \
"%s\n" % (failed_matcher, assertion_msg)
else:
best_matches_msg = "No similar requests, that have not been played, found."
return (
"Can't overwrite existing cassette (%r) in "
"your current record mode (%r).\n"
"No match for the request (%r) was found.\n"
"%s"
% (cassette._path, cassette.record_mode, failed_request, best_matches_msg)
)
class UnhandledHTTPRequestError(KeyError):

View File

@@ -8,35 +8,47 @@ log = logging.getLogger(__name__)
def method(r1, r2):
return r1.method == r2.method
assert r1.method == r2.method, "{} != {}".format(r1.method, r2.method)
def uri(r1, r2):
return r1.uri == r2.uri
assert r1.uri == r2.uri, "{} != {}".format(r1.uri, r2.uri)
def host(r1, r2):
return r1.host == r2.host
assert r1.host == r2.host, "{} != {}".format(r1.host, r2.host)
def scheme(r1, r2):
return r1.scheme == r2.scheme
assert r1.scheme == r2.scheme, "{} != {}".format(r1.scheme, r2.scheme)
def port(r1, r2):
return r1.port == r2.port
assert r1.port == r2.port, "{} != {}".format(r1.port, r2.port)
def path(r1, r2):
return r1.path == r2.path
assert r1.path == r2.path, "{} != {}".format(r1.path, r2.path)
def query(r1, r2):
return r1.query == r2.query
assert r1.query == r2.query, "{} != {}".format(r1.query, r2.query)
def raw_body(r1, r2):
return read_body(r1) == read_body(r2)
assert read_body(r1) == read_body(r2)
def body(r1, r2):
transformer = _get_transformer(r1)
r2_transformer = _get_transformer(r2)
if transformer != r2_transformer:
transformer = _identity
assert transformer(read_body(r1)) == transformer(read_body(r2))
def headers(r1, r2):
assert r1.headers == r2.headers, "{} != {}".format(r1.headers, r2.headers)
def _header_checker(value, header='Content-Type'):
@@ -56,9 +68,12 @@ def _transform_json(body):
_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),
(_header_checker('application/x-www-form-urlencoded'),
lambda body: urllib.parse.parse_qs(body.decode('ascii'))),
(_header_checker('application/json'),
_transform_json),
(lambda request: _xml_header_checker(request) and _xmlrpc_header_checker(request),
xmlrpc_client.loads),
)
@@ -74,28 +89,54 @@ def _get_transformer(request):
return _identity
def body(r1, r2):
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 {} and {} differ according to "
"the following matchers: {}".format(r1, r2, differences)
)
def requests_match(r1, r2, matchers):
matches = [(m(r1, r2), m) for m in matchers]
_log_matches(r1, r2, matches)
return all(m[0] for m in matches)
successes, failures = get_matchers_results(r1, r2, matchers)
if failures:
log.debug(
"Requests {} and {} differ.\n"
"Failure details:\n"
"{}".format(r1, r2, failures)
)
return len(failures) == 0
def _evaluate_matcher(matcher_function, *args):
"""
Evaluate the result of a given matcher as a boolean with an assertion error message if any.
It handles two types of matcher :
- a matcher returning a boolean value.
- a matcher that only makes an assert, returning None or raises an assertion error.
"""
assertion_message = None
try:
match = matcher_function(*args)
match = True if match is None else match
except AssertionError as e:
match = False
assertion_message = str(e)
return match, assertion_message
def get_matchers_results(r1, r2, matchers):
"""
Get the comparison results of two requests as two list.
The first returned list represents the matchers names that passed.
The second list is the failed matchers as a string with failed assertion details if any.
"""
matches_success, matches_fails = [], []
for m in matchers:
matcher_name = m.__name__
match, assertion_message = _evaluate_matcher(m, r1, r2)
if match:
matches_success.append(matcher_name)
else:
assertion_message = get_assertion_message(assertion_message)
matches_fails.append((matcher_name, assertion_message))
return matches_success, matches_fails
def get_assertion_message(assertion_details):
"""
Get a detailed message about the failing matcher.
"""
return assertion_details

View File

@@ -68,7 +68,7 @@ def _migrate(data):
for item in data:
req = item['request']
res = item['response']
uri = dict((k, req.pop(k)) for k in PARTS)
uri = {k: req.pop(k) for k in PARTS}
req['uri'] = build_uri(**uri)
# convert headers to dict of lists
headers = req['headers']
@@ -100,7 +100,7 @@ def migrate_json(in_fp, out_fp):
def _list_of_tuples_to_dict(fs):
return dict((k, v) for k, v in fs[0])
return {k: v for k, v in fs[0]}
def _already_migrated(data):
@@ -159,9 +159,9 @@ def main():
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("[{}] {}\n".format(status, file_path))
migrated = try_migrate(file_path)
status = 'OK' if migrated else 'FAIL'
sys.stderr.write("[{}] {}\n".format(status, file_path))
sys.stderr.write("Done.\n")

View File

@@ -6,21 +6,29 @@ from .compat import contextlib, mock
from .stubs import VCRHTTPConnection, VCRHTTPSConnection
from six.moves import http_client as httplib
import logging
log = logging.getLogger(__name__)
# Save some of the original types for the purposes of unpatching
_HTTPConnection = httplib.HTTPConnection
_HTTPSConnection = httplib.HTTPSConnection
# Try to save the original types for boto3
try:
import botocore.vendored.requests.packages.urllib3.connectionpool as cpool
except ImportError: # pragma: no cover
pass
from botocore.awsrequest import AWSHTTPSConnection, AWSHTTPConnection
except ImportError:
try:
import botocore.vendored.requests.packages.urllib3.connectionpool as cpool
except ImportError: # pragma: no cover
pass
else:
_Boto3VerifiedHTTPSConnection = cpool.VerifiedHTTPSConnection
_cpoolBoto3HTTPConnection = cpool.HTTPConnection
_cpoolBoto3HTTPSConnection = cpool.HTTPSConnection
else:
_Boto3VerifiedHTTPSConnection = cpool.VerifiedHTTPSConnection
_cpoolBoto3HTTPConnection = cpool.HTTPConnection
_cpoolBoto3HTTPSConnection = cpool.HTTPSConnection
_Boto3VerifiedHTTPSConnection = AWSHTTPSConnection
_cpoolBoto3HTTPConnection = AWSHTTPConnection
_cpoolBoto3HTTPSConnection = AWSHTTPSConnection
cpool = None
# Try to save the original types for urllib3
@@ -44,7 +52,6 @@ else:
_cpoolHTTPConnection = cpool.HTTPConnection
_cpoolHTTPSConnection = cpool.HTTPSConnection
# Try to save the original types for httplib2
try:
import httplib2
@@ -55,7 +62,6 @@ else:
_HTTPSConnectionWithTimeout = httplib2.HTTPSConnectionWithTimeout
_SCHEME_TO_CONNECTION = httplib2.SCHEME_TO_CONNECTION
# Try to save the original types for boto
try:
import boto.https_connection
@@ -64,7 +70,6 @@ except ImportError: # pragma: no cover
else:
_CertValidatingHTTPSConnection = boto.https_connection.CertValidatingHTTPSConnection
# Try to save the original types for Tornado
try:
import tornado.simple_httpclient
@@ -74,7 +79,6 @@ else:
_SimpleAsyncHTTPClient_fetch_impl = \
tornado.simple_httpclient.SimpleAsyncHTTPClient.fetch_impl
try:
import tornado.curl_httpclient
except ImportError: # pragma: no cover
@@ -99,6 +103,7 @@ class CassettePatcherBuilder(object):
return self._build_patchers_from_mock_triples(
function(self, *args, **kwargs)
)
return wrapped
def __init__(self, cassette):
@@ -184,13 +189,26 @@ class CassettePatcherBuilder(object):
return ()
return self._urllib3_patchers(cpool, requests_stubs)
@_build_patchers_from_mock_triples_decorator
def _boto3(self):
try:
import botocore.vendored.requests.packages.urllib3.connectionpool as cpool
# botocore using awsrequest
import botocore.awsrequest as cpool
except ImportError: # pragma: no cover
return ()
from .stubs import boto3_stubs
return self._urllib3_patchers(cpool, boto3_stubs)
try:
# botocore using vendored requests
import botocore.vendored.requests.packages.urllib3.connectionpool as cpool
except ImportError: # pragma: no cover
pass
else:
from .stubs import boto3_stubs
yield self._urllib3_patchers(cpool, boto3_stubs)
else:
from .stubs import boto3_stubs
log.debug("Patching boto3 cpool with %s", cpool)
yield cpool.AWSHTTPConnectionPool, 'ConnectionCls', boto3_stubs.VCRRequestsHTTPConnection
yield cpool.AWSHTTPSConnectionPool, 'ConnectionCls', boto3_stubs.VCRRequestsHTTPSConnection
def _patched_get_conn(self, connection_pool_class, connection_class_getter):
get_conn = connection_pool_class._get_conn
@@ -407,22 +425,36 @@ def reset_patchers():
yield mock.patch.object(cpool.HTTPSConnectionPool, 'ConnectionCls', _cpoolHTTPSConnection)
try:
import botocore.vendored.requests.packages.urllib3.connectionpool as cpool
# unpatch botocore with awsrequest
import botocore.awsrequest as cpool
except ImportError: # pragma: no cover
pass
try:
# unpatch botocore with vendored requests
import botocore.vendored.requests.packages.urllib3.connectionpool as cpool
except ImportError: # pragma: no cover
pass
else:
# unpatch requests v1.x
yield mock.patch.object(cpool, 'VerifiedHTTPSConnection', _Boto3VerifiedHTTPSConnection)
yield mock.patch.object(cpool, 'HTTPConnection', _cpoolBoto3HTTPConnection)
# unpatch requests v2.x
if hasattr(cpool.HTTPConnectionPool, 'ConnectionCls'):
yield mock.patch.object(cpool.HTTPConnectionPool, 'ConnectionCls',
_cpoolBoto3HTTPConnection)
yield mock.patch.object(cpool.HTTPSConnectionPool, 'ConnectionCls',
_cpoolBoto3HTTPSConnection)
if hasattr(cpool, 'HTTPSConnection'):
yield mock.patch.object(cpool, 'HTTPSConnection', _cpoolBoto3HTTPSConnection)
else:
# unpatch requests v1.x
yield mock.patch.object(cpool, 'VerifiedHTTPSConnection', _Boto3VerifiedHTTPSConnection)
yield mock.patch.object(cpool, 'HTTPConnection', _cpoolBoto3HTTPConnection)
# unpatch requests v2.x
if hasattr(cpool.HTTPConnectionPool, 'ConnectionCls'):
yield mock.patch.object(cpool.HTTPConnectionPool, 'ConnectionCls',
if hasattr(cpool.AWSHTTPConnectionPool, 'ConnectionCls'):
yield mock.patch.object(cpool.AWSHTTPConnectionPool, 'ConnectionCls',
_cpoolBoto3HTTPConnection)
yield mock.patch.object(cpool.HTTPSConnectionPool, 'ConnectionCls',
yield mock.patch.object(cpool.AWSHTTPSConnectionPool, 'ConnectionCls',
_cpoolBoto3HTTPSConnection)
if hasattr(cpool, 'HTTPSConnection'):
yield mock.patch.object(cpool, 'HTTPSConnection', _cpoolBoto3HTTPSConnection)
if hasattr(cpool, 'AWSHTTPSConnection'):
yield mock.patch.object(cpool, 'AWSHTTPSConnection', _cpoolBoto3HTTPSConnection)
try:
import httplib2 as cpool

View File

@@ -2,6 +2,9 @@ import warnings
from six import BytesIO, text_type
from six.moves.urllib.parse import urlparse, parse_qsl
from .util import CaseInsensitiveDict
import logging
log = logging.getLogger(__name__)
class Request(object):
@@ -18,6 +21,7 @@ class Request(object):
else:
self.body = body
self.headers = headers
log.debug("Invoking Request %s", self.uri)
@property
def headers(self):
@@ -58,7 +62,10 @@ class Request(object):
parse_uri = urlparse(self.uri)
port = parse_uri.port
if port is None:
port = {'https': 443, 'http': 80}[parse_uri.scheme]
try:
port = {'https': 443, 'http': 80}[parse_uri.scheme]
except KeyError:
pass
return port
@property
@@ -91,7 +98,7 @@ class Request(object):
'method': self.method,
'uri': self.uri,
'body': self.body,
'headers': dict(((k, [v]) for k, v in self.headers.items())),
'headers': {k: [v] for k, v in self.headers.items()},
}
@classmethod
@@ -112,7 +119,7 @@ class HeadersDict(CaseInsensitiveDict):
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
Furthermore, 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

View File

@@ -25,5 +25,5 @@ def serialize(cassette_dict):
original.end,
original.args[-1] + error_message
)
except TypeError as original: # py3
except TypeError: # py3
raise TypeError(error_message)

View File

@@ -60,9 +60,10 @@ def serialize_headers(response):
class VCRHTTPResponse(HTTPResponse):
"""
Stub reponse class that gets returned instead of a HTTPResponse
Stub response class that gets returned instead of a HTTPResponse
"""
def __init__(self, recorded_response):
self.fp = None
self.recorded_response = recorded_response
self.reason = recorded_response['status']['message']
self.status = self.code = recorded_response['status']['code']
@@ -93,9 +94,30 @@ class VCRHTTPResponse(HTTPResponse):
def read(self, *args, **kwargs):
return self._content.read(*args, **kwargs)
def readall(self):
return self._content.readall()
def readinto(self, *args, **kwargs):
return self._content.readinto(*args, **kwargs)
def readline(self, *args, **kwargs):
return self._content.readline(*args, **kwargs)
def readlines(self, *args, **kwargs):
return self._content.readlines(*args, **kwargs)
def seekable(self):
return self._content.seekable()
def tell(self):
return self._content.tell()
def isatty(self):
return self._content.isatty()
def seek(self, *args, **kwargs):
return self._content.seek(*args, **kwargs)
def close(self):
self._closed = True
return True
@@ -121,6 +143,9 @@ class VCRHTTPResponse(HTTPResponse):
else:
return default
def readable(self):
return self._content.readable()
class VCRConnection(object):
# A reference to the cassette that's currently being patched in
@@ -139,12 +164,13 @@ class VCRConnection(object):
if url and not url.startswith('/'):
# Then this must be a proxy request.
return url
uri = "{0}://{1}{2}{3}".format(
uri = "{}://{}{}{}".format(
self._protocol,
self.real_connection.host,
self._port_postfix(),
url,
)
log.debug("Absolute URI: %s", uri)
return uri
def _url(self, uri):
@@ -230,11 +256,8 @@ class VCRConnection(object):
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._vcr_request, self.cassette._path,
self.cassette.record_mode)
cassette=self.cassette,
failed_request=self._vcr_request
)
# Otherwise, we should send the request, then get the response

View File

@@ -3,13 +3,21 @@ from __future__ import absolute_import
import asyncio
import functools
import logging
import json
from aiohttp import ClientResponse
from aiohttp import ClientResponse, streams
from multidict import CIMultiDict, CIMultiDictProxy
from yarl import URL
from vcr.request import Request
log = logging.getLogger(__name__)
class MockStream(asyncio.StreamReader, streams.AsyncStreamReaderMixin):
pass
class MockClientResponse(ClientResponse):
def __init__(self, method, url):
@@ -26,26 +34,89 @@ class MockClientResponse(ClientResponse):
)
async def json(self, *, encoding='utf-8', loads=json.loads, **kwargs): # NOQA: E999
return loads(self._body.decode(encoding))
stripped = self._body.strip()
if not stripped:
return None
async def text(self, encoding='utf-8'):
return self._body.decode(encoding)
return loads(stripped.decode(encoding))
async def text(self, encoding='utf-8', errors='strict'):
return self._body.decode(encoding, errors=errors)
async def read(self):
return self._body
async def release(self):
def release(self):
pass
@property
def content(self):
s = MockStream()
s.feed_data(self._body)
s.feed_eof()
return s
def build_response(vcr_request, vcr_response, history):
response = MockClientResponse(vcr_request.method, URL(vcr_response.get('url')))
response.status = vcr_response['status']['code']
response._body = vcr_response['body'].get('string', b'')
response.reason = vcr_response['status']['message']
response._headers = CIMultiDictProxy(CIMultiDict(vcr_response['headers']))
response._history = tuple(history)
response.close()
return response
def play_responses(cassette, vcr_request):
history = []
vcr_response = cassette.play_response(vcr_request)
response = build_response(vcr_request, vcr_response, history)
while cassette.can_play_response_for(vcr_request):
history.append(response)
vcr_response = cassette.play_response(vcr_request)
response = build_response(vcr_request, vcr_response, history)
return response
async def record_response(cassette, vcr_request, response, past=False):
body = {} if past else {'string': (await response.read())}
headers = {str(key): value for key, value in response.headers.items()}
vcr_response = {
'status': {
'code': response.status,
'message': response.reason,
},
'headers': headers,
'body': body, # NOQA: E999
'url': str(response.url),
}
cassette.append(vcr_request, vcr_response)
async def record_responses(cassette, vcr_request, response):
for past_response in response.history:
await record_response(cassette, vcr_request, past_response, past=True)
await record_response(cassette, vcr_request, response)
def vcr_request(cassette, real_request):
@functools.wraps(real_request)
async def new_request(self, method, url, **kwargs):
headers = kwargs.get('headers')
auth = kwargs.get('auth')
headers = self._prepare_headers(headers)
data = kwargs.get('data')
data = kwargs.get('data', kwargs.get('json'))
params = kwargs.get('params')
if auth is not None:
headers['AUTHORIZATION'] = auth.encode()
request_url = URL(url)
if params:
for k, v in params.items():
@@ -55,16 +126,7 @@ def vcr_request(cassette, real_request):
vcr_request = Request(method, str(request_url), data, headers)
if cassette.can_play_response_for(vcr_request):
vcr_response = cassette.play_response(vcr_request)
response = MockClientResponse(method, URL(vcr_response.get('url')))
response.status = vcr_response['status']['code']
response._body = vcr_response['body']['string']
response.reason = vcr_response['status']['message']
response._headers = vcr_response['headers']
response.close()
return response
return play_responses(cassette, vcr_request)
if cassette.write_protected and cassette.filter_request(vcr_request):
response = MockClientResponse(method, URL(url))
@@ -76,19 +138,10 @@ def vcr_request(cassette, real_request):
response.close()
return response
log.info('%s not in cassette, sending to real server', vcr_request)
response = await real_request(self, method, url, **kwargs) # NOQA: E999
vcr_response = {
'status': {
'code': response.status,
'message': response.reason,
},
'headers': dict(response.headers),
'body': {'string': (await response.read())}, # NOQA: E999
'url': response.url,
}
cassette.append(vcr_request, vcr_response)
await record_responses(cassette, vcr_request, response)
return response
return new_request

View File

@@ -1,11 +1,22 @@
'''Stubs for boto3'''
"""Stubs for boto3"""
import six
try:
# boto using awsrequest
from botocore.awsrequest import AWSHTTPConnection as HTTPConnection
from botocore.awsrequest import AWSHTTPSConnection as VerifiedHTTPSConnection
except ImportError: # pragma: nocover
# boto using vendored requests
# urllib3 defines its own HTTPConnection classes, which boto3 goes ahead and assumes
# you're using. It includes some polyfills for newer features missing in older pythons.
try:
from urllib3.connectionpool import HTTPConnection, VerifiedHTTPSConnection
except ImportError: # pragma: nocover
from requests.packages.urllib3.connectionpool import HTTPConnection, VerifiedHTTPSConnection
from botocore.vendored.requests.packages.urllib3.connectionpool import HTTPConnection, VerifiedHTTPSConnection
from ..stubs import VCRHTTPConnection, VCRHTTPSConnection
# urllib3 defines its own HTTPConnection classes, which boto3 goes ahead and assumes
# you're using. It includes some polyfills for newer features missing in older pythons.
class VCRRequestsHTTPConnection(VCRHTTPConnection, HTTPConnection):
_baseclass = HTTPConnection
@@ -13,3 +24,20 @@ class VCRRequestsHTTPConnection(VCRHTTPConnection, HTTPConnection):
class VCRRequestsHTTPSConnection(VCRHTTPSConnection, VerifiedHTTPSConnection):
_baseclass = VerifiedHTTPSConnection
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 force_reset
with force_reset():
self.real_connection = self._baseclass(*args, **kwargs)
# Make sure to set those attributes as it seems `AWSHTTPConnection` does not
# set them, making the connection to fail !
self.real_connection.assert_hostname = kwargs.get("assert_hostname", False)
self.real_connection.cert_reqs = kwargs.get("cert_reqs", 'CERT_NONE')
self._sock = None

View File

@@ -75,10 +75,8 @@ def vcr_fetch_impl(cassette, real_fetch_impl):
request,
599,
error=CannotOverwriteExistingCassetteException(
"No match for the request (%r) was found. "
"Can't overwrite existing cassette (%r) in "
"your current record mode (%r)."
% (vcr_request, cassette._path, cassette.record_mode)
cassette=cassette,
failed_request=vcr_request
),
request_time=self.io_loop.time() - request.start_time,
)

View File

@@ -1,13 +1,17 @@
import collections
import types
try:
from collections.abc import Mapping, MutableMapping
except ImportError:
from collections import Mapping, MutableMapping
# Shamelessly stolen from https://github.com/kennethreitz/requests/blob/master/requests/structures.py
class CaseInsensitiveDict(collections.MutableMapping):
class CaseInsensitiveDict(MutableMapping):
"""
A case-insensitive ``dict``-like object.
Implements all methods and operations of
``collections.MutableMapping`` as well as dict's ``copy``. Also
``collections.abc.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)``,
@@ -57,7 +61,7 @@ class CaseInsensitiveDict(collections.MutableMapping):
)
def __eq__(self, other):
if isinstance(other, collections.Mapping):
if isinstance(other, Mapping):
other = CaseInsensitiveDict(other)
else:
return NotImplemented
@@ -114,10 +118,10 @@ def auto_decorate(
)
def __new__(cls, name, bases, attributes_dict):
new_attributes_dict = dict(
(attribute, maybe_decorate(attribute, value))
new_attributes_dict = {
attribute: maybe_decorate(attribute, value)
for attribute, value in attributes_dict.items()
)
}
return super(DecorateAll, cls).__new__(
cls, name, bases, new_attributes_dict
)