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

Compare commits

...

66 Commits

Author SHA1 Message Date
Kevin McCarthy
c79a06f639 release version 4.1.1 2020-10-09 15:38:37 -05:00
Jair Henrique
4991d3d22b Fix tornado tests 2020-10-05 09:45:55 -05:00
Jair Henrique
b2193828ab Fix matrix of tests 2020-10-05 09:45:55 -05:00
Jair Henrique
9d414c8841 Fix lint erros 2020-10-05 09:45:55 -05:00
Jair Henrique
a172207df0 Fix integration with httpx>0.15.0 2020-10-05 09:45:55 -05:00
Aaron Robson
1d0fdbaa13 fix #429: include a trailing newline in the JSON cassette format
It is a common convention for text files (esp. in Linux) to end with a newline.
2020-08-05 17:27:30 -05:00
Kevin McCarthy
a351621d92 update changelog and bump version 2020-07-31 09:40:27 -05:00
Hernan Ezequiel Di Giorgi
f387950486 support allow_redirects option 2020-07-24 15:34:57 -05:00
Hernan Ezequiel Di Giorgi
8529c46f00 update tests
- Use mockbin for redirections.
- Mock synchronous requests too.
2020-07-24 15:34:57 -05:00
Gintaras Jak
5afa8f703a Ignore utf8 decoding errors in content 2020-07-24 15:34:57 -05:00
Hernan Ezequiel Di Giorgi
641d9e5d49 list httpx in the compatibility list 2020-07-24 15:34:57 -05:00
Martynas Mickevičius
0ef400195b Fix linter 2020-07-24 15:34:57 -05:00
Martynas Mickevičius
133423ce94 Reuse value from Location header on redirect
Because the recoded requests are using original value.
Changing the value on redirect prevents finding the
matching response.
2020-07-24 15:34:57 -05:00
Martynas Mickevičius
7d2d29de12 Extract cookies to client on every response
This is required because previous extraction code is now patched out by vcpy.

Also handle headers with same key in responses.
2020-07-24 15:34:57 -05:00
Martynas Mickevičius
023e41bb4c Run tests on Travis CI 2020-07-24 15:34:57 -05:00
Hernan Ezequiel Di Giorgi
6ae93ac820 fix linting errors in test_httpx.py 2020-07-24 15:34:57 -05:00
Hernan Ezequiel Di Giorgi
04b7f4fc65 test that works behind proxy 2020-07-24 15:34:57 -05:00
Hernan Ezequiel Di Giorgi
936feb7748 fix redirection url 2020-07-24 15:34:57 -05:00
Hernan Ezequiel Di Giorgi
79d26ebb43 make it work with gzipped data 2020-07-24 15:34:57 -05:00
Hernan Ezequiel Di Giorgi
b8024de1b8 add missing pytest-asyncio dep for httpx 2020-07-24 15:34:57 -05:00
Hernan Ezequiel Di Giorgi
2f94d06e9b add httpx support 2020-07-24 15:34:57 -05:00
Tyson Holub
042ee790e2 add allow_playback_repeats option to Cassette 2020-07-15 16:09:17 -05:00
Paulo Romeira
a249781b97 aiohttp: Use record_mode enum in tests 2020-07-15 13:52:00 -05:00
Paulo Romeira
b64e93aff2 aiohttp: Check if the response is playable before redirecting 2020-07-15 13:52:00 -05:00
Paulo Romeira
4897a8e692 aiohttp: Fix linting 2020-07-15 13:52:00 -05:00
Paulo Romeira
15d79e5b78 aiphttp: Add test for cookies on redirect 2020-07-15 13:52:00 -05:00
Paulo Romeira
1b9f80d741 aiohttp: Ignore cookie expiration date 2020-07-15 13:52:00 -05:00
Paulo Romeira
20fb283e97 aiohttp: Update cookies with redirection responses 2020-07-15 13:52:00 -05:00
Paulo Romeira
e868b64922 aiohttp: Fix next_url redirect for absolute paths 2020-07-15 13:52:00 -05:00
Paulo Romeira
69cecdbda7 aiohttp: Fix tests 2020-07-15 13:52:00 -05:00
Paulo Romeira
be53091ae5 aiohttp: Replace aiohttp package for module 2020-07-15 13:52:00 -05:00
Paulo Romeira
ba91053485 aiohttp: Raise CannotOverwriteExistingCassette if not found 2020-07-15 13:52:00 -05:00
Paulo Romeira
837992767e aiohttp: Fix tests 2020-07-15 13:52:00 -05:00
Paulo Romeira
cd0907ffaf aiohttp: Serialize request headers 2020-07-15 13:52:00 -05:00
Paulo Romeira
77d838e0fc aiohttp: Fix code linting 2020-07-15 13:52:00 -05:00
Paulo Romeira
5362db2ebb aiohttp: Update tests 2020-07-15 13:52:00 -05:00
Paulo Romeira
f9ce14d29a aiohttp: Fix vcr_request cookies 2020-07-15 13:52:00 -05:00
Paulo Romeira
5242e68cd1 aiohttp: Fix code linting 2020-07-15 13:52:00 -05:00
Paulo Romeira
9817a8bda5 aiohttp: Add request cookies to "Cookie" header 2020-07-15 13:52:00 -05:00
Paulo Romeira
6e1768b85b aiohttp: Update ClientSession's cookie_jar with response cookies 2020-07-15 13:52:00 -05:00
Paulo Romeira
062126e50c tests/aiohttp: Add cookies test 2020-07-15 13:52:00 -05:00
Paulo Romeira
438550959f aiohttp: Add response cookies loading 2020-07-15 13:52:00 -05:00
Paulo Romeira
69e4316545 aiohttp: Add support for multiple headers with same key 2020-07-15 13:52:00 -05:00
Jair Henrique
2f53776ffb Use mockbin to fix redirect test 2020-07-15 12:18:23 -05:00
Ivan Malison
535efe1eb9 Merge pull request #514 from timgates42/bugfix/typo_cassette
Fix simple typo: cassete -> cassette
2020-07-01 20:19:41 -07:00
Ivan Malison
eb2e226bb8 Merge pull request #530 from aaronbannin/record-mode-enum
Enumerate Record Mode Values
2020-07-01 20:08:06 -07:00
Ivan Malison
8fe2ab6d06 Merge pull request #511 from royjs/master
Do not redirect on 304 for aiohttp
2020-06-29 14:25:45 -07:00
Aaron Bannin
6ac535f18d fix lints 2020-05-24 21:12:41 -07:00
Aaron Bannin
bceaab8b88 add carraige returns 2020-05-24 20:44:21 -07:00
Aaron Bannin
0c2bbe0d51 enumerate record_mode values 2020-05-24 20:15:01 -07:00
Kevin McCarthy
2e5fdd36d5 Update README.rst 2020-04-19 08:46:10 -05:00
Tim Gates
f8b9a41f13 Fix simple typo: cassete -> cassette
Closes #513
2020-02-26 18:04:31 +11:00
Jean-Sebastien Roy
6e040030b8 Do not redirect on 304 for aiohttp 2020-01-27 11:24:02 -05:00
Josh Peak
accffa8ea2 Correct mock imports. Fixes #504 (#505)
* Correct mock imports. Fixes #504

Update changelog

Revert accidental import deletion

* Remove move dependency that was masking error from tox.ini
2019-12-20 22:08:25 +11:00
Josh Peak
f07083e7cc Fix Logo alignment for PYPI description 2019-12-20 12:11:01 +11:00
Josh Peak
4495dbd1ce Fixed README syntax errors thanks to and 2019-12-20 12:04:36 +11:00
Josh Peak
357e3b03c5 Revert to PNG for PYPI upload 2019-12-20 11:55:36 +11:00
Josh Peak
566a70ab3a Updated link to logo in README so it animates 2019-12-20 11:48:08 +11:00
Josh Peak
4a8e80ee3e v4.0.x - Remove legacy python and add python3.8 support (#499)
* Drop support for legacy Python 2.7

* Upgrade Python syntax with pyupgrade --py3-plus

* Trim testing matrix to remove python2

* re-enable python3.8 in travis as tests that are not allowed to fail

* Remove some six

* The future is now

* Remove Python 2 imports

* Add back example, but change py27 to py36

* Remove redundant compat.py

* Blacken

* Credit hugovk in changelog

WIP Updating Sphinx Docs and AutoDoc

* Fix AutoDoc and update Sphinx theme to python_doc_theme

* Fix #420, autodoc even undocumented (docstring-less) method signatures

* Doc theme 'nature'. Add global TOC to doc sidebar

* Comment last reference to package six

* Changelog is now a consistent format

* Yet another documentation fix for links and title hierarchy

* Start work on new SVG logo

test SVG in README

trying to test new SVG logo in README

Apply centering

Apply readme logo centering

Trying to align image

Trying random shit

trying align right

add emoji

Large logo has higher priority

Change title hierarchy

Actually use a H1

Try again

try and organise badges

revert link back to point at master

* updated new take on VCR logo as SVG code

* Testing modern logo in docs

* Add sanitize for rendering SVG

* Switch to alabaster theme

* Update vcrpy logo (#503)

* Add credit for V4 logo changes.

* Add rewind and play animation

* Add svg into ReadTheDocs static assets so that it can be hosted so the animations work.

* Need to embedd the SVG for ReadTheDocs somewhere so I can get the link to later embed in the README

Co-authored-by: Hugo van Kemenade <hugovk@users.noreply.github.com>
Co-authored-by: Sean Bailey <sean@seanbailey.io>
2019-12-20 11:45:07 +11:00
Josh Peak
41ca5750e7 Fix heading hierarchy in debugging guide 2019-12-19 13:16:12 +11:00
Josh Peak
d91a53bbff Fix italics emphasis in contributing guide 2019-12-19 13:15:19 +11:00
Josh Peak
88092eb3cd Update contributing guide to explain non-code contributions 2019-12-19 13:14:17 +11:00
Josh Peak
4fcc5472bc Update changelog.rst
Correct format of link to milestones for documentation
2019-12-14 20:37:01 +11:00
Josh Peak
8245bd4f84 v3.0.0 RC (#498)
* v3.0.0 RC

* Add credit for documentation improvements for @yarikoptic
2019-12-14 19:49:50 +11:00
Yaroslav Halchenko
531685d50b DOC(FIX): fixed variable name (a -> cass) in an example for rewind (#492) 2019-12-13 15:16:42 +11:00
Nick DiRienzo
9ec19dd966 aiohttp: fix multiple requests being replayed per request and add support for request_info on mocked responses (#495)
* Fix how redirects are handled so we can have requests with the same URL be used distinctly

* Add support for request_info. Remove `past` kwarg.

* Remove as e to make linter happy

* Add unreleased 3.0.0 to changelog.
2019-12-13 15:01:17 +11:00
66 changed files with 1631 additions and 662 deletions

View File

@@ -1,44 +1,26 @@
dist: xenial dist: xenial
language: python language: python
before_install: openssl version
env:
# global:
# - secure: AifoKzwhjV94cmcQZrdQmqRu/9rkZZvWpwBv1daeAQpLOKFPGsOm3D+x2cSw9+iCfkgDZDfqQVv1kCaFVxTll8v8jTq5SJdqEY0NmGWbj/UkNtShh609oRDsuzLxAEwtVKYjf/h8K2BRea+bl1tGkwZ2vtmYS6dxNlAijjWOfds=
# - secure: LBSEg/gMj4u4Hrpo3zs6Y/1mTpd2RtcN49mZIFgTdbJ9IhpiNPqcEt647Lz94F9Eses2x2WbNuKqZKZZReY7QLbEzU1m0nN5jlaKrjcG5NR5clNABfFFyhgc0jBikyS4abAG8jc2efeaTrFuQwdoF4sE8YiVrkiVj2X5Xoi6sBk=
matrix:
- TOX_SUFFIX="requests"
- TOX_SUFFIX="httplib2"
- TOX_SUFFIX="boto3"
- TOX_SUFFIX="urllib3"
- TOX_SUFFIX="tornado4"
- TOX_SUFFIX="aiohttp"
matrix: matrix:
include: include:
# Only run lint on a single 3.x # Only run lint on a single 3.x
- env: TOX_SUFFIX="lint" - env: TOX_SUFFIX="lint"
python: "3.7" python: "3.7"
allow_failures:
- env: TOX_SUFFIX="aiohttp"
python: "pypy3"
- python: "3.8-dev"
exclude:
# Exclude aiohttp support
- env: TOX_SUFFIX="aiohttp"
python: "2.7"
- env: TOX_SUFFIX="aiohttp"
python: "pypy"
python: python:
- "2.7"
- "3.5" - "3.5"
- "3.6" - "3.6"
- "3.7" - "3.7"
- "3.8-dev" - "3.8"
- "pypy"
- "pypy3" - "pypy3"
before_install:
- openssl version
- sudo apt-get install libgnutls28-dev
install: install:
- pip install tox-travis codecov - pip install tox-travis codecov
- if [[ $TOX_SUFFIX != 'lint' ]]; then python setup.py install ; fi - if [[ $TOX_SUFFIX != 'lint' ]]; then python setup.py install ; fi
script: script:
- tox -e "${TOX_SUFFIX}" - tox
after_success: after_success:
- codecov - codecov

View File

@@ -1,10 +1,16 @@
###########
VCR.py 📼
###########
|PyPI| |Python versions| |Build Status| |CodeCov| |Gitter| |CodeStyleBlack| |PyPI| |Python versions| |Build Status| |CodeCov| |Gitter| |CodeStyleBlack|
VCR.py ----
======
.. image:: https://vcrpy.readthedocs.io/en/latest/_images/vcr.svg
:alt: vcr.py logo
.. image:: https://raw.github.com/kevin1024/vcrpy/master/vcr.png
:alt: vcr.py
This is a Python version of `Ruby's VCR This is a Python version of `Ruby's VCR
library <https://github.com/vcr/vcr>`__. library <https://github.com/vcr/vcr>`__.
@@ -41,8 +47,13 @@ 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 all HTTP interactions, which will update them to correspond to the new
API. API.
Usage with Pytest
-----------------
There is a library to provide some pytest fixtures called pytest-recording https://github.com/kiwicom/pytest-recording
License License
======= -------
This library uses the MIT license. See `LICENSE.txt <LICENSE.txt>`__ for This library uses the MIT license. See `LICENSE.txt <LICENSE.txt>`__ for
more details more details

BIN
docs/_static/vcr.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

27
docs/_static/vcr.svg vendored Normal file
View File

@@ -0,0 +1,27 @@
<svg width="634" height="346" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0 10C0 4.477 4.477 0 10 0h614c5.523 0 10 4.477 10 10v326c0 5.523-4.477 10-10 10H10c-5.523 0-10-4.477-10-10V10z" fill="#181B19"/>
<rect x="36" y="83" width="561" height="234" rx="19" fill="#262927"/>
<mask id="a" maskUnits="userSpaceOnUse" x="66" y="104" width="503" height="192">
<path d="M161.434 120h310.431m-229.757 80c0 44.183-35.841 80-80.054 80S82 244.183 82 200s35.841-80 80.054-80 80.054 35.817 80.054 80zm310.432 0c0 44.183-35.841 80-80.054 80s-80.054-35.817-80.054-80 35.841-80 80.054-80 80.054 35.817 80.054 80z" stroke="#27DD7C" stroke-width="31" stroke-dasharray="503" stroke-dashoffset="0">
<animate attributeName="stroke-dashoffset" to="503" begin="1s" dur="1s"/>
<animate attributeName="stroke" to="#0" begin="1s" dur="1s"/>
<animate attributeName="stroke-dashoffset" from="503" to="0" begin="2s" dur="3s" />
<animate attributeName="stroke" from="#000" to="#27DD7C" begin="2s" dur="3s"/>
</path>
</mask>
<g mask="url(#a)">
<path fill="url(#paint0_linear)" d="M64 102h507v196H64z"/>
</g>
<path d="M0 10C0 4.477 4.477 0 10 0h614c5.523 0 10 4.477 10 10v40H0V10z" fill="#262927"/>
<rect x="189" y="6" width="256" height="38" rx="2" fill="#fff"/>
<path stroke="#E6E8E6" d="M198 14.5h238M198 25.5h238M198 36.5h238"/>
<path d="M261.207 18.61c-.443 0-.762-.098-.957-.294-.182-.195-.273-.54-.273-1.035 0-.494.091-.84.273-1.035.195-.195.514-.293.957-.293h6.914c.443 0 .755.098.938.293.195.195.293.54.293 1.035 0 .495-.098.84-.293 1.035-.183.196-.495.293-.938.293h-2.5l5.332 12.735 5.391-12.735h-1.699c-.443 0-.762-.097-.957-.293-.183-.195-.274-.54-.274-1.035 0-.494.091-.84.274-1.035.195-.195.514-.293.957-.293h6.132c.443 0 .756.098.938.293.195.195.293.54.293 1.035 0 .495-.098.84-.293 1.035-.182.196-.495.293-.938.293h-1.23l-6.309 14.551c-.182.443-.449.762-.8.957-.352.209-.853.313-1.504.313s-1.146-.105-1.485-.313c-.338-.208-.599-.527-.781-.957l-6.25-14.55h-1.211zm38.136 3.847a3.73 3.73 0 00-.352-1.621 3.392 3.392 0 00-1.054-1.27c-.456-.364-1.022-.644-1.7-.84-.677-.208-1.464-.312-2.363-.312-.95 0-1.829.163-2.637.488a6.03 6.03 0 00-2.05 1.367 6.354 6.354 0 00-1.348 2.09c-.325.795-.488 1.667-.488 2.618 0 .95.15 1.829.449 2.636a6.197 6.197 0 001.27 2.07 5.858 5.858 0 002.011 1.368c.781.325 1.647.488 2.598.488 1.198 0 2.357-.182 3.476-.547a10.295 10.295 0 003.086-1.62c.339-.261.658-.359.957-.294.3.065.586.326.86.781.221.352.312.716.273 1.094-.026.378-.228.703-.605.977-1.055.78-2.285 1.393-3.692 1.836a14.417 14.417 0 01-4.355.664c-1.432 0-2.728-.235-3.887-.703-1.159-.482-2.148-1.146-2.969-1.993a8.805 8.805 0 01-1.894-2.988c-.443-1.159-.664-2.415-.664-3.77 0-1.354.228-2.604.683-3.75a9.008 9.008 0 011.914-3.007 8.783 8.783 0 012.969-1.973c1.159-.482 2.442-.723 3.848-.723 1.185 0 2.259.215 3.223.645a6.591 6.591 0 012.441 1.797v-1.172c0-.456.104-.781.312-.977.222-.195.606-.293 1.153-.293s.924.098 1.133.293c.221.196.332.521.332.977v5.664c0 .456-.111.781-.332.977-.209.195-.586.293-1.133.293s-.931-.098-1.153-.293c-.208-.196-.312-.521-.312-.977zm12.394-3.848h-2.481c-.442 0-.761-.097-.957-.293-.182-.195-.273-.54-.273-1.035 0-.494.091-.84.273-1.035.196-.195.515-.293.957-.293h3.985c.442 0 .755.098.937.293.195.195.293.54.293 1.035v3.946a13.604 13.604 0 011.641-2.364 11.627 11.627 0 011.992-1.797 8.754 8.754 0 012.187-1.132 6.569 6.569 0 012.246-.41 5.53 5.53 0 012.852.78c.378.222.619.495.723.821.104.326.065.73-.117 1.21-.183.47-.417.802-.704.997-.273.195-.599.182-.976-.039a4.257 4.257 0 00-2.051-.527c-.586 0-1.185.11-1.797.332a7.489 7.489 0 00-1.758.898c-.56.378-1.087.84-1.582 1.387a10.586 10.586 0 00-1.289 1.777 9.542 9.542 0 00-.859 2.09 8.5 8.5 0 00-.313 2.305v3.789h6.582c.443 0 .756.097.938.293.195.195.293.54.293 1.035 0 .495-.098.84-.293 1.035-.182.195-.495.293-.938.293H308.67c-.442 0-.761-.098-.957-.293-.182-.195-.273-.54-.273-1.035 0-.495.091-.84.273-1.035.196-.196.515-.293.957-.293h3.067V18.609zm19.561 0h-1.601c-.443 0-.762-.097-.957-.293-.182-.195-.274-.54-.274-1.035 0-.494.092-.84.274-1.035.195-.195.514-.293.957-.293h3.105c.443 0 .756.098.938.293.195.195.293.54.293 1.035v1.563c.807-1.055 1.745-1.869 2.812-2.442 1.081-.586 2.272-.879 3.575-.879 1.21 0 2.33.209 3.359.625a7.4 7.4 0 012.656 1.758c.755.768 1.348 1.706 1.778 2.813.429 1.107.644 2.363.644 3.77 0 1.405-.215 2.662-.644 3.769-.43 1.107-1.023 2.044-1.778 2.812a7.62 7.62 0 01-2.656 1.778c-1.029.403-2.149.605-3.359.605-1.303 0-2.468-.26-3.497-.781-1.015-.534-1.914-1.29-2.695-2.266v8.614h4.336c.443 0 .755.097.938.292.195.196.293.54.293 1.036 0 .494-.098.84-.293 1.035-.183.195-.495.293-.938.293h-9.453c-.443 0-.762-.098-.957-.293-.182-.196-.274-.54-.274-1.035 0-.495.092-.84.274-1.035.195-.196.514-.294.957-.294h2.187V18.61zm2.93 5.88c0 .963.15 1.822.449 2.577.3.756.716 1.394 1.25 1.915a5.536 5.536 0 001.895 1.171c.742.274 1.543.41 2.402.41.886 0 1.673-.15 2.364-.449.69-.3 1.269-.716 1.738-1.25a5.696 5.696 0 001.074-1.933 7.692 7.692 0 00.371-2.442c0-.885-.124-1.699-.371-2.441a5.485 5.485 0 00-1.074-1.914 4.738 4.738 0 00-1.738-1.27c-.691-.3-1.478-.449-2.364-.449-.859 0-1.66.137-2.402.41a5.391 5.391 0 00-1.895 1.192c-.534.507-.95 1.139-1.25 1.894-.299.755-.449 1.615-.449 2.578zm37.687-8.536c.443 0 .755.098.937.293.196.195.293.54.293 1.035 0 .495-.097.84-.293 1.035-.182.196-.494.293-.937.293h-1.211l-7.988 16.368c-.625 1.276-1.237 2.356-1.836 3.242-.599.885-1.244 1.608-1.934 2.168s-1.452.97-2.285 1.23c-.82.274-1.764.43-2.832.469-.378.013-.697-.072-.957-.254-.248-.182-.397-.534-.449-1.055-.052-.573.013-.983.195-1.23.195-.247.469-.384.82-.41.899-.052 1.66-.176 2.285-.371a4.375 4.375 0 001.739-.977c.521-.469 1.028-1.12 1.523-1.953.508-.82 1.087-1.888 1.738-3.203l-7.148-14.024h-1.23c-.443 0-.762-.097-.958-.293-.182-.195-.273-.54-.273-1.035 0-.494.091-.84.273-1.035.196-.195.515-.293.958-.293h6.914c.442 0 .755.098.937.293.195.195.293.54.293 1.035 0 .495-.098.84-.293 1.035-.182.196-.495.293-.937.293h-2.5l5.507 10.977 5.254-10.977h-1.738c-.443 0-.762-.097-.957-.293-.182-.195-.273-.54-.273-1.035 0-.494.091-.84.273-1.035.195-.195.514-.293.957-.293h6.133z" fill="#262927"/>
<defs>
<linearGradient id="paint0_linear" x1="64" y1="298" x2="544.524" y2="50.579" gradientUnits="userSpaceOnUse">
<stop stop-color="#27DD70"/>
<stop offset="1" stop-color="#27DDA6"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 6.2 KiB

View File

@@ -33,6 +33,8 @@ consider part of the API. The fields are as follows:
been played back. been played back.
- ``responses_of(request)``: Access the responses that match a given - ``responses_of(request)``: Access the responses that match a given
request request
- ``allow_playback_repeats``: A boolean indicating whether responses
can be played back more than once.
The ``Request`` object has the following properties: The ``Request`` object has the following properties:
@@ -130,11 +132,11 @@ Finally, register your method with VCR to use your new request matcher.
Register your own cassette persister Register your own cassette persister
------------------------------------ ------------------------------------
Create your own persistence class, see the :ref:`persister_example`. Create your own persistence class, see the example below:
Your custom persister must implement both ``load_cassette`` and ``save_cassette`` Your custom persister must implement both ``load_cassette`` and ``save_cassette``
methods. The ``load_cassette`` method must return a deserialized cassette or raise methods. The ``load_cassette`` method must return a deserialized cassette or raise
``ValueError`` if no cassette is found. ``ValueError`` if no cassette is found.
Once the persister class is defined, register with VCR like so... Once the persister class is defined, register with VCR like so...
@@ -143,7 +145,7 @@ Once the persister class is defined, register with VCR like so...
import vcr import vcr
my_vcr = vcr.VCR() my_vcr = vcr.VCR()
class CustomerPersister(object): class CustomerPersister:
# implement Persister methods... # implement Persister methods...
my_vcr.register_persister(CustomPersister) my_vcr.register_persister(CustomPersister)
@@ -384,5 +386,21 @@ VCR.py allows to rewind a cassette in order to replay it inside the same functio
with vcr.use_cassette('fixtures/vcr_cassettes/synopsis.yaml') as cass: with vcr.use_cassette('fixtures/vcr_cassettes/synopsis.yaml') as cass:
response = urllib2.urlopen('http://www.zombo.com/').read() response = urllib2.urlopen('http://www.zombo.com/').read()
assert cass.all_played assert cass.all_played
a.rewind() cass.rewind()
assert not cass.all_played assert not cass.all_played
Playback Repeats
----------------
By default, each response in a cassette can only be matched and played back
once while the cassette is in use, unless the cassette is rewound.
If you want to allow playback repeats without rewinding the cassette, use
the Cassette ``allow_playback_repeats`` option.
.. code:: python
with vcr.use_cassette('fixtures/vcr_cassettes/synopsis.yaml', allow_playback_repeats=True) as cass:
for x in range(10):
response = urllib2.urlopen('http://www.zombo.com/').read()
assert cass.all_played

View File

@@ -1,197 +1,238 @@
Changelog Changelog
--------- ---------
- 2.1.x (UNRELEASED)
- .... For a full list of triaged issues, bugs and PRs and what release they are targetted for please see the following link.
`ROADMAP MILESTONES <https://github.com/kevin1024/vcrpy/milestones>`_
All help in providing PRs to close out bug issues is appreciated. Even if that is providing a repo that fully replicates issues. We have very generous contributors that have added these to bug issues which meant another contributor picked up the bug and closed it out.
- 4.1.1
- Fix HTTPX support for versions greater than 0.15 (thanks @jairhenrique)
- Include a trailing newline on json cassettes (thanks @AaronRobson)
- 4.1.0
- Add support for httpx!! (thanks @herdigiorgi)
- Add the new `allow_playback_repeats` option (thanks @tysonholub)
- Several aiohttp improvements (cookie support, multiple headers with same key) (Thanks @pauloromeira)
- Use enums for record modes (thanks @aaronbannin)
- Bugfix: Do not redirect on 304 in aiohttp (Thanks @royjs)
- Bugfix: Fix test suite by switching to mockbin (thanks @jairhenrique)
- 4.0.2
- Fix mock imports as reported in #504 by @llybin. Thank you.
- 4.0.1
- Fix logo alignment for PyPI
- 4.0.0
- Remove Python2 support (@hugovk)
- Add Python 3.8 TravisCI support (@neozenith)
- Updated the logo to a modern material design (@sean0x42)
- 3.0.0
- This release is a breaking change as it changes how aiohttp follows redirects and your cassettes may need to be re-recorded with this update.
- Fix multiple requests being replayed per single request in aiohttp stub #495 (@nickdirienzo)
- Add support for `request_info` on mocked responses in aiohttp stub #495 (@nickdirienzo)
- doc: fixed variable name (a -> cass) in an example for rewind #492 (@yarikoptic)
- 2.1.1 - 2.1.1
- Format code with black (@neozenith) - Format code with black (@neozenith)
- Use latest pypy3 in Travis (@hugovk) - Use latest pypy3 in Travis (@hugovk)
- Improve documentation about custom matchers (@gward) - Improve documentation about custom matchers (@gward)
- Fix exception when body is empty (@keithprickett) - Fix exception when body is empty (@keithprickett)
- Add `pytest-recording` to the documentation as an alternative Pytest plugin (@Stranger6667) - Add `pytest-recording` to the documentation as an alternative Pytest plugin (@Stranger6667)
- Fix yarl and python3.5 version issue (@neozenith) - Fix yarl and python3.5 version issue (@neozenith)
- Fix header matcher for boto3 - fixes #474 (@simahawk) - Fix header matcher for boto3 - fixes #474 (@simahawk)
- 2.1.0 - Add a `rewind` method to reset a cassette (thanks @khamidou) - 2.1.0
New error message with more details on why the cassette failed to play a request (thanks @arthurHamon2, @neozenith) - Add a `rewind` method to reset a cassette (thanks @khamidou)
Handle connect tunnel URI (thanks @jeking3) - New error message with more details on why the cassette failed to play a request (thanks @arthurHamon2, @neozenith)
Add code coverage to the project (thanks @neozenith) - Handle connect tunnel URI (thanks @jeking3)
Drop support to python 3.4 - Add code coverage to the project (thanks @neozenith)
Add deprecation warning on python 2.7, next major release will drop python 2.7 support - Drop support to python 3.4
Fix build problems on requests tests (thanks to @dunossauro) - Add deprecation warning on python 2.7, next major release will drop python 2.7 support
Fix matching on 'body' failing when Unicode symbols are present in them (thanks @valgur) - Fix build problems on requests tests (thanks to @dunossauro)
Fix bugs on aiohttp integration (thanks @graingert, @steinnes, @stj, @lamenezes, @lmazuel) - Fix matching on 'body' failing when Unicode symbols are present in them (thanks @valgur)
Fix Biopython incompatibility (thanks @rishab121) - Fix bugs on aiohttp integration (thanks @graingert, @steinnes, @stj, @lamenezes, @lmazuel)
Fix Boto3 integration (thanks @1oglop1, @arthurHamon2) - Fix Biopython incompatibility (thanks @rishab121)
- 2.0.1 - Fix bug when using vcrpy with python 3.4 - Fix Boto3 integration (thanks @1oglop1, @arthurHamon2)
- 2.0.0 - Support python 3.7 (fix httplib2 and urllib2, thanks @felixonmars) - 2.0.1
[#356] Fixes `before_record_response` so the original response isn't changed (thanks @kgraves) - Fix bug when using vcrpy with python 3.4
Fix requests stub when using proxy (thanks @samuelfekete @daneoshiga) - 2.0.0
(only for aiohttp stub) Drop support to python 3.4 asyncio.coroutine (aiohttp doesn't support python it anymore) - Support python 3.7 (fix httplib2 and urllib2, thanks @felixonmars)
Fix aiohttp stub to work with aiohttp client (thanks @stj) - [#356] Fixes `before_record_response` so the original response isn't changed (thanks @kgraves)
Fix aiohttp stub to accept content type passed - Fix requests stub when using proxy (thanks @samuelfekete @daneoshiga)
Improve docs (thanks @adamchainz) - (only for aiohttp stub) Drop support to python 3.4 asyncio.coroutine (aiohttp doesn't support python it anymore)
- 1.13.0 - Fix support to latest aiohttp version (3.3.2). Fix content-type bug in aiohttp stub. Save URL with query params properly when using aiohttp. - Fix aiohttp stub to work with aiohttp client (thanks @stj)
- 1.12.0 - Fix support to latest aiohttp version (3.2.1), Adapted setup to PEP508, Support binary responses on aiohttp, Dropped support for EOL python versions (2.6 and 3.3) - Fix aiohttp stub to accept content type passed
- 1.11.1 Fix compatibility with newest requests and urllib3 releases - Improve docs (thanks @adamchainz)
- 1.11.0 Allow injection of persistence methods + bugfixes (thanks @j-funk and @IvanMalison),
Support python 3.6 + CI tests (thanks @derekbekoe and @graingert),
Support pytest-asyncio coroutines (thanks @graingert) - 1.13.0
- 1.10.5 Added a fix to httplib2 (thanks @carlosds730), Fix an issue with - Fix support to latest aiohttp version (3.3.2). Fix content-type bug in aiohttp stub. Save URL with query params properly when using aiohttp.
aiohttp (thanks @madninja), Add missing requirement yarl (thanks @lamenezes), - 1.12.0
Remove duplicate mock triple (thanks @FooBarQuaxx) - Fix support to latest aiohttp version (3.2.1), Adapted setup to PEP508, Support binary responses on aiohttp, Dropped support for EOL python versions (2.6 and 3.3)
- 1.10.4 Fix an issue with asyncio aiohttp (thanks @madninja) - 1.11.1
- 1.10.3 Fix some issues with asyncio and params (thanks @anovikov1984 and - Fix compatibility with newest requests and urllib3 releases
@lamenezes), Fix some issues with cassette serialize / deserialize and empty - 1.11.0
response bodies (thanks @gRoussac and @dz0ny) - Allow injection of persistence methods + bugfixes (thanks @j-funk and @IvanMalison),
- 1.10.2 Fix 1.10.1 release - add aiohttp support back in - Support python 3.6 + CI tests (thanks @derekbekoe and @graingert),
- 1.10.1 [bad release] Fix build for Fedora package + python2 (thanks @puiterwijk and @lamenezes) - Support pytest-asyncio coroutines (thanks @graingert)
- 1.10.0 Add support for aiohttp (thanks @lamenezes) - 1.10.5
- 1.9.0 Add support for boto3 (thanks @desdm, @foorbarna). Fix deepcopy issue - Added a fix to httplib2 (thanks @carlosds730), Fix an issue with
for response headers when `decode_compressed_response` is enabled (thanks - aiohttp (thanks @madninja), Add missing requirement yarl (thanks @lamenezes),
@nickdirienzo) - Remove duplicate mock triple (thanks @FooBarQuaxx)
- 1.8.0 Fix for Serialization errors with JSON adapter (thanks - 1.10.4
@aliaksandrb). Avoid concatenating bytes with strings (thanks - Fix an issue with asyncio aiohttp (thanks @madninja)
@jaysonsantos). Exclude __pycache__ dirs & compiled files in sdist - 1.10.3
(thanks @koobs). Fix Tornado support behavior for Tornado 3 (thanks - Fix some issues with asyncio and params (thanks @anovikov1984 and @lamenezes)
@abhinav). decode_compressed_response option and filter (thanks - Fix some issues with cassette serialize / deserialize and empty response bodies (thanks @gRoussac and @dz0ny)
@jayvdb). - 1.10.2
- 1.7.4 [#217] Make use_cassette decorated functions actually return a - Fix 1.10.1 release - add aiohttp support back in
value (thanks @bcen). [#199] Fix path transfromation defaults. - 1.10.1
Better headers dictionary management. - [bad release] Fix build for Fedora package + python2 (thanks @puiterwijk and @lamenezes)
- 1.7.3 [#188] ``additional_matchers`` kwarg on ``use_cassette``. - 1.10.0
[#191] Actually support passing multiple before_record_request - Add support for aiohttp (thanks @lamenezes)
functions (thanks @agriffis). - 1.9.0
- 1.7.2 [#186] Get effective_url in tornado (thanks @mvschaik), [#187] - Add support for boto3 (thanks @desdm, @foorbarna).
Set request_time on Response object in tornado (thanks @abhinav). - Fix deepcopy issue for response headers when `decode_compressed_response` is enabled (thanks @nickdirienzo)
- 1.7.1 [#183] Patch ``fetch_impl`` instead of the entire HTTPClient - 1.8.0
class for Tornado (thanks @abhinav). - Fix for Serialization errors with JSON adapter (thanks @aliaksandrb).
- 1.7.0 [#177] Properly support coroutine/generator decoration. [#178] - Avoid concatenating bytes with strings (thanks @jaysonsantos).
Support distribute (thanks @graingert). [#163] Make compatibility - Exclude __pycache__ dirs & compiled files in sdist (thanks @koobs).
between python2 and python3 recorded cassettes more robust (thanks - Fix Tornado support behavior for Tornado 3 (thanks @abhinav).
@gward). - decode_compressed_response option and filter (thanks @jayvdb).
- 1.6.1 [#169] Support conditional requirements in old versions of - 1.7.4 [#217]
pip, Fix RST parse errors generated by pandoc, [Tornado] Fix - Make use_cassette decorated functions actually return a value (thanks @bcen).
unsupported features exception not being raised, [#166] - [#199] Fix path transfromation defaults.
content-aware body matcher. - Better headers dictionary management.
- 1.6.0 [#120] Tornado support (thanks @abhinav), [#147] packaging fixes - 1.7.3 [#188]
(thanks @graingert), [#158] allow filtering post params in requests - ``additional_matchers`` kwarg on ``use_cassette``.
(thanks @MrJohz), [#140] add xmlrpclib support (thanks @Diaoul). - [#191] Actually support passing multiple before_record_request functions (thanks @agriffis).
- 1.5.2 Fix crash when cassette path contains cassette library - 1.7.2
directory (thanks @gazpachoking). - [#186] Get effective_url in tornado (thanks @mvschaik)
- 1.5.0 Automatic cassette naming and 'application/json' post data - [#187] Set request_time on Response object in tornado (thanks @abhinav).
filtering (thanks @marco-santamaria). - 1.7.1
- 1.4.2 Fix a bug caused by requests 2.7 and chunked transfer encoding - [#183] Patch ``fetch_impl`` instead of the entire HTTPClient class for Tornado (thanks @abhinav).
- 1.4.1 Include README, tests, LICENSE in package. Thanks @ralphbean. - 1.7.0
- 1.4.0 Filter post data parameters (thanks @eadmundo), support for - [#177] Properly support coroutine/generator decoration.
posting files through requests, inject\_cassette kwarg to access - [#178] Support distribute (thanks @graingert). [#163] Make compatibility between python2 and python3 recorded cassettes more robust (thanks @gward).
cassette from ``use_cassette`` decorated function, - 1.6.1
``with_current_defaults`` actually works (thanks @samstav). - [#169] Support conditional requirements in old versions of pip
- 1.3.0 Fix/add support for urllib3 (thanks @aisch), fix default port - Fix RST parse errors generated by pandoc
for https (thanks @abhinav). - [Tornado] Fix unsupported features exception not being raised
- 1.2.0 Add custom\_patches argument to VCR/Cassette objects to allow - [#166] content-aware body matcher.
users to stub custom classes when cassettes become active. - 1.6.0
- 1.1.4 Add force reset around calls to actual connection from stubs, - [#120] Tornado support (thanks @abhinav)
to ensure compatibility with the version of httplib/urlib2 in python - [#147] packaging fixes (thanks @graingert)
2.7.9. - [#158] allow filtering post params in requests (thanks @MrJohz)
- 1.1.3 Fix python3 headers field (thanks @rtaboada), fix boto test - [#140] add xmlrpclib support (thanks @Diaoul).
(thanks @telaviv), fix new\_episodes record mode (thanks @jashugan), - 1.5.2
fix Windows connectionpool stub bug (thanks @gazpachoking), add - Fix crash when cassette path contains cassette library directory (thanks @gazpachoking).
support for requests 2.5 - 1.5.0
- 1.1.2 Add urllib==1.7.1 support. Make json serialize error handling - Automatic cassette naming and 'application/json' post data filtering (thanks @marco-santamaria).
correct Improve logging of match failures. - 1.4.2
- 1.1.1 Use function signature preserving ``wrapt.decorator`` to write - Fix a bug caused by requests 2.7 and chunked transfer encoding
the decorator version of use\_cassette in order to ensure - 1.4.1
compatibility with py.test fixtures and python 2. Move all request - Include README, tests, LICENSE in package. Thanks @ralphbean.
filtering into the ``before_record_callable``. - 1.4.0
- 1.1.0 Add ``before_record_response``. Fix several bugs related to the - Filter post data parameters (thanks @eadmundo)
context management of cassettes. - 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.0.3: Fix an issue with requests 2.4 and make sure case sensitivity - 1.3.0
is consistent across python versions - Fix/add support for urllib3 (thanks @aisch)
- 1.0.2: Fix an issue with requests 2.3 - Fix default port for https (thanks @abhinav).
- 1.0.1: Fix a bug with the new ignore requests feature and the once - 1.2.0
record mode - Add custom\_patches argument to VCR/Cassette objects to allow users to stub custom classes when cassettes become active.
- 1.0.0: *BACKWARDS INCOMPATIBLE*: Please see the 'upgrade' section in - 1.1.4
the README. Take a look at the matcher section as well, you might - Add force reset around calls to actual connection from stubs, to ensure compatibility with the version of httplib/urlib2 in python 2.7.9.
want to update your ``match_on`` settings. Add support for filtering - 1.1.3
sensitive data from requests, matching query strings after the order - Fix python3 headers field (thanks @rtaboada)
changes and improving the built-in matchers, (thanks to @mshytikov), - fix boto test (thanks @telaviv)
support for ignoring requests to certain hosts, bump supported - fix new\_episodes record mode (thanks @jashugan),
Python3 version to 3.4, fix some bugs with Boto support (thanks - fix Windows connectionpool stub bug (thanks @gazpachoking)
@marusich), fix error with URL field capitalization in README (thanks - add support for requests 2.5
@simon-weber), added some log messages to help with debugging, added - 1.1.2
``all_played`` property on cassette (thanks @mshytikov) - Add urllib==1.7.1 support.
- 0.7.0: VCR.py now supports Python 3! (thanks @asundg) Also I - Make json serialize error handling correct
refactored the stub connections quite a bit to add support for the - Improve logging of match failures.
putrequest and putheader calls. This version also adds support for - 1.1.1
httplib2 (thanks @nilp0inter). I have added a couple tests for boto - 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.
since it is an http client in its own right. Finally, this version - Move all request filtering into the ``before_record_callable``.
includes a fix for a bug where requests wasn't being patched properly - 1.1.0
(thanks @msabramo). - Add ``before_record_response``. Fix several bugs related to the context management of cassettes.
- 0.6.0: Store response headers as a list since a HTTP response can - 1.0.3
have the same header twice (happens with set-cookie sometimes). This - Fix an issue with requests 2.4 and make sure case sensitivity is consistent across python versions
has the added benefit of preserving the order of headers. Thanks - 1.0.2
@smallcode for the bug report leading to this change. I have made an - Fix an issue with requests 2.3
effort to ensure backwards compatibility with the old cassettes' - 1.0.1
header storage mechanism, but if you want to upgrade to the new - Fix a bug with the new ignore requests feature and the once record mode
header storage, you should delete your cassettes and re-record them. - 1.0.0
Also this release adds better error messages (thanks @msabramo) and - *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.
adds support for using VCR as a decorator (thanks @smallcode for the - Add support for filtering sensitive data from requests, matching query strings after the order changes and improving the built-in matchers, (thanks to @mshytikov)
motivation) - Support for ignoring requests to certain hosts, bump supported Python3 version to 3.4, fix some bugs with Boto support (thanks @marusich)
- 0.5.0: Change the ``response_of`` method to ``responses_of`` since - Fix error with URL field capitalization in README (thanks @simon-weber)
cassettes can now contain more than one response for a request. Since - Added some log messages to help with debugging
this changes the API, I'm bumping the version. Also includes 2 - Added ``all_played`` property on cassette (thanks @mshytikov)
bugfixes: a better error message when attempting to overwrite a
cassette file, and a fix for a bug with requests sessions (thanks - 0.7.0
@msabramo) - VCR.py now supports Python 3! (thanks @asundg)
- 0.4.0: Change default request recording behavior for multiple - Also I refactored the stub connections quite a bit to add support for the putrequest and putheader calls.
requests. If you make the same request multiple times to the same - This version also adds support for httplib2 (thanks @nilp0inter).
URL, the response might be different each time (maybe the response - I have added a couple tests for boto since it is an http client in its own right.
has a timestamp in it or something), so this will make the same - Finally, this version includes a fix for a bug where requests wasn't being patched properly (thanks @msabramo).
request multiple times and save them all. Then, when you are - 0.6.0
replaying the cassette, the responses will be played back in the same - Store response headers as a list since a HTTP response can have the same header twice (happens with set-cookie sometimes).
order in which they were received. If you were making multiple - This has the added benefit of preserving the order of headers.
requests to the same URL in a cassette before version 0.4.0, you - Thanks @smallcode for the bug report leading to this change.
might need to regenerate your cassette files. Also, removes support - 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.
for the cassette.play\_count counter API, since individual requests - Also this release adds better error messages (thanks @msabramo)
aren't unique anymore. A cassette might contain the same request - and adds support for using VCR as a decorator (thanks @smallcode for the motivation)
several times. Also removes secure overwrite feature since that was - 0.5.0
breaking overwriting files in Windows, and fixes a bug preventing - Change the ``response_of`` method to ``responses_of`` since cassettes can now contain more than one response for a request.
request's automatic body decompression from working. - Since this changes the API, I'm bumping the version.
- 0.3.5: Fix compatibility with requests 2.x - Also includes 2 bugfixes:
- 0.3.4: Bugfix: close file before renaming it. This fixes an issue on - a better error message when attempting to overwrite a cassette file,
Windows. Thanks @smallcode for the fix. - and a fix for a bug with requests sessions (thanks @msabramo)
- 0.3.3: Bugfix for error message when an unreigstered custom matcher - 0.4.0
was used - Change default request recording behavior for multiple requests.
- 0.3.2: Fix issue with new config syntax and the ``match_on`` - 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.
parameter. Thanks, @chromy! - Then, when you are replaying the cassette, the responses will be played back in the same order in which they were received.
- 0.3.1: Fix issue causing full paths to be sent on the HTTP request - 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.
line. - Also, removes support for the cassette.play\_count counter API, since individual requests aren't unique anymore.
- 0.3.0: *Backwards incompatible release* - Added support for record - A cassette might contain the same request several times.
modes, and changed the default recording behavior to the "once" - Also removes secure overwrite feature since that was breaking overwriting files in Windows
record mode. Please see the documentation on record modes for more. - And fixes a bug preventing request's automatic body decompression from working.
Added support for custom request matching, and changed the default - 0.3.5
request matching behavior to match only on the URL and method. Also, - Fix compatibility with requests 2.x
improved the httplib mocking to add support for the - 0.3.4
``HTTPConnection.send()`` method. This means that requests won't - Bugfix: close file before renaming it. This fixes an issue on Windows. Thanks @smallcode for the fix.
actually be sent until the response is read, since I need to record - 0.3.3
the entire request in order to match up the appropriate response. I - Bugfix for error message when an unreigstered custom matcher was used
don't think this should cause any issues unless you are sending - 0.3.2
requests without ever loading the response (which none of the - Fix issue with new config syntax and the ``match_on`` parameter. Thanks, @chromy!
standard httplib wrappers do, as far as I know. Thanks to @fatuhoku - 0.3.1
for some of the ideas and the motivation behind this release. - Fix issue causing full paths to be sent on the HTTP request line.
- 0.2.1: Fixed missing modules in setup.py - 0.3.0
- 0.2.0: Added configuration API, which lets you configure some - *Backwards incompatible release*
settings on VCR (see the README). Also, VCR no longer saves cassettes - 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.
if they haven't changed at all and supports JSON as well as YAML - Added support for custom request matching, and changed the default request matching behavior to match only on the URL and method.
(thanks @sirpengi). Added amazing new skeumorphic logo, thanks - Also, improved the httplib mocking to add support for the ``HTTPConnection.send()`` method.
@hairarrow. - 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.
- 0.1.0: *backwards incompatible release - delete your old cassette - 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).
files*: This release adds the ability to access the cassette to make - Thanks to @fatuhoku for some of the ideas and the motivation behind this release.
assertions on it, as well as a major code refactor thanks to - 0.2.1
@dlecocq. It also fixes a couple longstanding bugs with redirects and - Fixed missing modules in setup.py
HTTPS. [#3 and #4] - 0.2.0
- 0.0.4: If you have libyaml installed, vcrpy will use the c bindings - Added configuration API, which lets you configure some settings on VCR (see the README).
instead. Speed up your tests! Thanks @dlecocq - Also, VCR no longer saves cassettes if they haven't changed at all and supports JSON as well as YAML (thanks @sirpengi).
- 0.0.3: Add support for requests 1.2.3. Support for older versions of - Added amazing new skeumorphic logo, thanks @hairarrow.
requests dropped (thanks @vitormazzi and @bryanhelmig) - 0.1.0
- 0.0.2: Add support for requests / urllib3 - *backwards incompatible release - delete your old cassette files*
- 0.0.1: Initial Release - 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

View File

@@ -12,8 +12,33 @@
# All configuration values have a default; values that are commented out # All configuration values have a default; values that are commented out
# serve to show the default. # serve to show the default.
import codecs
import os import os
import re
here = os.path.abspath(os.path.dirname(__file__))
def read(*parts):
# intentionally *not* adding an encoding option to open, See:
# https://github.com/pypa/virtualenv/issues/201#issuecomment-3145690
with codecs.open(os.path.join(here, *parts), "r") as fp:
return fp.read()
def find_version(*file_paths):
version_file = read(*file_paths)
version_match = re.search(r"^__version__ = ['\"]([^'\"]*)['\"]", version_file, re.M)
if version_match:
return version_match.group(1)
raise RuntimeError("Unable to find version string.")
autodoc_default_options = {
"members": None,
"undoc-members": None,
}
# If extensions (or modules to document with autodoc) are in another directory, # If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the # add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here. # documentation root, use os.path.abspath to make it absolute, like shown here.
@@ -27,7 +52,14 @@ import os
# Add any Sphinx extension module names here, as strings. They can be # Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones. # ones.
extensions = ["sphinx.ext.autodoc", "sphinx.ext.intersphinx", "sphinx.ext.coverage", "sphinx.ext.viewcode"] extensions = [
"sphinx.ext.autodoc",
"sphinx.ext.intersphinx",
"sphinx.ext.coverage",
"sphinx.ext.viewcode",
"sphinx.ext.todo",
"sphinx.ext.githubpages",
]
# Add any paths that contain templates here, relative to this directory. # Add any paths that contain templates here, relative to this directory.
templates_path = ["_templates"] templates_path = ["_templates"]
@@ -44,18 +76,18 @@ source_suffix = ".rst"
master_doc = "index" master_doc = "index"
# General information about the project. # General information about the project.
project = u"vcrpy" project = "vcrpy"
copyright = u"2015, Kevin McCarthy" copyright = "2015, Kevin McCarthy"
author = u"Kevin McCarthy" author = "Kevin McCarthy"
# The version info for the project you're documenting, acts as replacement for # The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the # |version| and |release|, also used in various other places throughout the
# built documents. # built documents.
# #
# The short X.Y version. # The short X.Y version.
version = "1.7.4" # version = "1.7.4"
# The full version, including alpha/beta/rc tags. # The full version, including alpha/beta/rc tags.
release = "1.7.4" version = release = find_version("..", "vcr", "__init__.py")
# The language for content autogenerated by Sphinx. Refer to documentation # The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages. # for a list of supported languages.
@@ -106,11 +138,11 @@ todo_include_todos = False
# The theme to use for HTML and HTML Help pages. # The theme to use for HTML and HTML Help pages.
# https://read-the-docs.readthedocs.io/en/latest/theme.html#how-do-i-use-this-locally-and-on-read-the-docs # https://read-the-docs.readthedocs.io/en/latest/theme.html#how-do-i-use-this-locally-and-on-read-the-docs
if "READTHEDOCS" not in os.environ: # if "READTHEDOCS" not in os.environ:
import sphinx_rtd_theme # import sphinx_rtd_theme
#
html_theme = "sphinx_rtd_theme" # html_theme = "sphinx_rtd_theme"
html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] # html_theme_path = [sphinx_rtd_theme.get_html_theme_path()]
# Theme options are theme-specific and customize the look and feel of a theme # Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the # further. For a list of options available for each theme, see the
@@ -153,6 +185,8 @@ html_static_path = ["_static"]
# Custom sidebar templates, maps document names to template names. # Custom sidebar templates, maps document names to template names.
# html_sidebars = {} # html_sidebars = {}
html_sidebars = {"**": ["globaltoc.html", "relations.html", "sourcelink.html", "searchbox.html"]}
# Additional templates that should be rendered to pages, maps page names to # Additional templates that should be rendered to pages, maps page names to
# template names. # template names.
@@ -205,19 +239,21 @@ htmlhelp_basename = "vcrpydoc"
latex_elements = { latex_elements = {
# The paper size ('letterpaper' or 'a4paper'). # The paper size ('letterpaper' or 'a4paper').
#'papersize': 'letterpaper', # 'papersize': 'letterpaper',
# The font size ('10pt', '11pt' or '12pt'). # The font size ('10pt', '11pt' or '12pt').
#'pointsize': '10pt', # 'pointsize': '10pt',
# Additional stuff for the LaTeX preamble. # Additional stuff for the LaTeX preamble.
#'preamble': '', # 'preamble': '',
# Latex figure (float) alignment # Latex figure (float) alignment
#'figure_align': 'htbp', # 'figure_align': 'htbp',
} }
# Grouping the document tree into LaTeX files. List of tuples # Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title, # (source start file, target name, title,
# author, documentclass [howto, manual, or own class]). # author, documentclass [howto, manual, or own class]).
latex_documents = [(master_doc, "vcrpy.tex", u"vcrpy Documentation", u"Kevin McCarthy", "manual")] latex_documents = [
(master_doc, "vcrpy.tex", "vcrpy Documentation", "Kevin McCarthy", "manual"),
]
# The name of an image file (relative to this directory) to place at the top of # The name of an image file (relative to this directory) to place at the top of
# the title page. # the title page.
@@ -244,7 +280,7 @@ latex_documents = [(master_doc, "vcrpy.tex", u"vcrpy Documentation", u"Kevin McC
# One entry per manual page. List of tuples # One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section). # (source start file, name, description, authors, manual section).
man_pages = [(master_doc, "vcrpy", u"vcrpy Documentation", [author], 1)] man_pages = [(master_doc, "vcrpy", "vcrpy Documentation", [author], 1)]
# If true, show URL addresses after external links. # If true, show URL addresses after external links.
# man_show_urls = False # man_show_urls = False
@@ -259,12 +295,12 @@ texinfo_documents = [
( (
master_doc, master_doc,
"vcrpy", "vcrpy",
u"vcrpy Documentation", "vcrpy Documentation",
author, author,
"vcrpy", "vcrpy",
"One line description of project.", "One line description of project.",
"Miscellaneous", "Miscellaneous",
) ),
] ]
# Documents to append as an appendix to all manuals. # Documents to append as an appendix to all manuals.
@@ -282,3 +318,4 @@ texinfo_documents = [
# Example configuration for intersphinx: refer to the Python standard library. # Example configuration for intersphinx: refer to the Python standard library.
intersphinx_mapping = {"https://docs.python.org/": None} intersphinx_mapping = {"https://docs.python.org/": None}
html_theme = "alabaster"

View File

@@ -1,23 +1,106 @@
Contributing Contributing
============ ============
.. image:: _static/vcr.svg
:alt: vcr.py logo
:align: right
🚀 Milestones
--------------
For anyone interested in the roadmap and projected release milestones please see the following link:
`MILESTONES <https://github.com/kevin1024/vcrpy/milestones>`_
----
🎁 Contributing Issues and PRs
-------------------------------
- Issues and PRs will get triaged and assigned to the appropriate milestone.
- PRs get priority over issues.
- The maintainers have limited bandwidth and do so **voluntarily**.
So whilst reporting issues are valuable, please consider:
- contributing an issue with a toy repo that replicates the issue.
- contributing PRs is a more valuable donation of your time and effort.
Thanks again for your interest and support in VCRpy.
We really appreciate it.
----
👥 Collaborators
-----------------
We also have a large test matrix to cover and would like members to volunteer covering these roles.
============ ==================== ================= ================== ======================
**Library** **Issue Triager(s)** **Maintainer(s)** **PR Reviewer(s)** **Release Manager(s)**
------------ -------------------- ----------------- ------------------ ----------------------
``core`` Needs support Needs support Needs support @neozenith
``requests`` @neozenith Needs support @neozenith @neozenith
``aiohttp`` Needs support Needs support Needs support @neozenith
``urllib3`` Needs support Needs support Needs support @neozenith
``httplib2`` Needs support Needs support Needs support @neozenith
``tornado4`` Needs support Needs support Needs support @neozenith
``boto3`` Needs support Needs support Needs support @neozenith
============ ==================== ================= ================== ======================
Role Descriptions
~~~~~~~~~~~~~~~~~
**Issue Triager:**
Simply adding these three labels for incoming issues means a lot for maintaining this project:
- ``bug`` or ``enhancement``
- Which library does it affect? ``core``, ``aiohttp``, ``requests``, ``urllib3``, ``tornado4``, ``httplib2``
- If it is a bug, is it ``Verified Can Replicate`` or ``Requires Help Replicating``
- Thanking people for raising issues. Feedback is always appreciated.
- Politely asking if they are able to link to an example repo that replicates the issue if they haven't already. Being able to *clone and go* helps the next person and we like that. 😃
**Maintainer:**
This involves creating PRs to address bugs and enhancement requests. It also means maintaining the test suite, docstrings and documentation .
**PR Reviewer:**
The PR reviewer is a second set of eyes to see if:
- Are there tests covering the code paths added/modified?
- Do the tests and modifications make sense seem appropriate?
- Add specific feedback, even on approvals, why it is accepted. eg "I like how you use a context manager there. 😄 "
- Also make sure they add a line to `docs/changelog.rst` to claim credit for their contribution.
**Release Manager:**
- Ensure CI is passing.
- Create a release on github and tag it with the changelog release notes.
- ``python setup.py build sdist bdist_wheel``
- ``twine upload dist/*``
- Go to ReadTheDocs build page and trigger a build https://readthedocs.org/projects/vcrpy/builds/
----
Running VCR's test suite Running VCR's test suite
------------------------ ------------------------
The tests are all run automatically on `Travis The tests are all run automatically on `Travis
CI <https://travis-ci.org/kevin1024/vcrpy>`__, but you can also run them CI <https://travis-ci.org/kevin1024/vcrpy>`__, but you can also run them
yourself using `py.test <http://pytest.org/>`__ and yourself using `pytest <http://pytest.org/>`__ and
`Tox <http://tox.testrun.org/>`__. Tox will automatically run them in `Tox <http://tox.testrun.org/>`__.
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 will automatically run them in all environments VCR.py supports if they are available on your `PATH`. Alternatively you can use `tox-pyenv <https://pypi.org/project/tox-pyenv/>`_ with
`pyenv <https://github.com/pyenv/pyenv>`_.
We recommend you read the documentation for each and see the section further below.
The test suite is pretty big and slow, but you can tell tox to only run specific tests like this::
tox -e {pyNN}-{HTTP_LIBRARY} -- <pytest flags passed through> tox -e {pyNN}-{HTTP_LIBRARY} -- <pytest flags passed through>
tox -e py27-requests -- -v -k "'test_status_code or test_gzip'" tox -e py36-requests -- -v -k "'test_status_code or test_gzip'"
tox -e py37-requests -- -v --last-failed tox -e py37-requests -- -v --last-failed
This will run only tests that look like ``test_status_code`` or 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 ``test_gzip`` in the test suite, and only in the python 3.6 environment
that has ``requests`` installed. that has ``requests`` installed.
Also, in order for the boto tests to run, you will need an AWS key. Also, in order for the boto tests to run, you will need an AWS key.
@@ -31,8 +114,8 @@ Using PyEnv with VCR's test suite
--------------------------------- ---------------------------------
PyEnv is a tool for managing multiple installation of python on your system. PyEnv is a tool for managing multiple installation of python on your system.
See the full documentation at their `github <https://github.com/pyenv/pyenv>` See the full documentation at their `github <https://github.com/pyenv/pyenv>`_
but we are also going to use `tox-pyenv <https://pypi.org/project/tox-pyenv/>` but we are also going to use `tox-pyenv <https://pypi.org/project/tox-pyenv/>`_
in this example:: in this example::
git clone https://github.com/pyenv/pyenv ~/.pyenv git clone https://github.com/pyenv/pyenv ~/.pyenv
@@ -47,10 +130,10 @@ in this example::
pip install tox tox-pyenv pip install tox tox-pyenv
# Install supported versions (at time of writing), this does not activate them # Install supported versions (at time of writing), this does not activate them
pyenv install 2.7.10 3.5.7 3.6.9 3.7.4 3.8-dev pypy2.6-7.1.1 pypy3.6-7.1.1 pyenv install 3.5.9 3.6.9 3.7.5 3.8.0 pypy3.6-7.2.0
# This activates them # This activates them
pyenv local 2.7.10 3.5.7 3.6.9 3.7.4 3.8-dev pypy2.6-7.1.1 pypy3.6-7.1.1 pyenv local 3.5.9 3.6.9 3.7.5 3.8.0 pypy3.6-7.2.0
# Run the whole test suite # Run the whole test suite
tox tox

View File

@@ -31,7 +31,7 @@ which matchers didn't match. This can help you with debugging custom
matchers. matchers.
CannotOverwriteExistingCassetteException CannotOverwriteExistingCassetteException
======================================== ----------------------------------------
When a request failed to be found in an existing cassette, 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. VCR.py tries to get the request(s) that may be similar to the one being searched.
@@ -44,7 +44,7 @@ It can return multiple similar requests with :
CannotOverwriteExistingCassetteException message example : CannotOverwriteExistingCassetteException message example :
.. code:: python .. code::
CannotOverwriteExistingCassetteException: Can't overwrite existing cassette ('cassette.yaml') in your current record mode ('once'). 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. No match for the request (<Request (GET) https://www.googleapis.com/?alt=json&maxResults=200>) was found.

View File

@@ -4,7 +4,7 @@ Contents
======== ========
.. toctree:: .. toctree::
:maxdepth: 2 :maxdepth: 3
installation installation
usage usage
@@ -15,6 +15,7 @@ Contents
contributing contributing
changelog changelog
================== ==================
Indices and tables Indices and tables
================== ==================

View File

@@ -9,8 +9,7 @@ with pip::
Compatibility Compatibility
------------- -------------
VCR.py supports Python 2.7 and 3.5+, and VCR.py supports Python 3.5+, and `pypy <http://pypy.org>`__.
`pypy <http://pypy.org>`__.
The following HTTP libraries are supported: The following HTTP libraries are supported:
@@ -23,6 +22,7 @@ The following HTTP libraries are supported:
- ``tornado.httpclient`` - ``tornado.httpclient``
- ``urllib2`` - ``urllib2``
- ``urllib3`` - ``urllib3``
- ``httpx``
Speed Speed
----- -----

View File

@@ -4,4 +4,4 @@
# If you are getting an INVOCATION ERROR for this script then there is # If you are getting an INVOCATION ERROR for this script then there is
# a good chance you are running on Windows. # a good chance you are running on Windows.
# You can and should use WSL for running tox on Windows when it calls bash scripts. # 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 $* REQUESTS_CA_BUNDLE=`python -m pytest_httpbin.certs` pytest $*

View File

@@ -1,11 +1,31 @@
#!/usr/bin/env python #!/usr/bin/env python
import codecs
import os
import re
import sys import sys
from setuptools import setup, find_packages from setuptools import setup, find_packages
from setuptools.command.test import test as TestCommand from setuptools.command.test import test as TestCommand
long_description = open("README.rst", "r").read() long_description = open("README.rst", "r").read()
here = os.path.abspath(os.path.dirname(__file__))
def read(*parts):
# intentionally *not* adding an encoding option to open, See:
# https://github.com/pypa/virtualenv/issues/201#issuecomment-3145690
with codecs.open(os.path.join(here, *parts), "r") as fp:
return fp.read()
def find_version(*file_paths):
version_file = read(*file_paths)
version_match = re.search(r"^__version__ = ['\"]([^'\"]*)['\"]", version_file, re.M)
if version_match:
return version_match.group(1)
raise RuntimeError("Unable to find version string.")
class PyTest(TestCommand): class PyTest(TestCommand):
@@ -26,26 +46,21 @@ install_requires = [
"PyYAML", "PyYAML",
"wrapt", "wrapt",
"six>=1.5", "six>=1.5",
'contextlib2; python_version=="2.7"',
'mock; python_version=="2.7"',
'yarl; python_version>="3.6"', 'yarl; python_version>="3.6"',
'yarl<1.4; python_version=="3.5"', 'yarl<1.4; python_version=="3.5"',
] ]
excluded_packages = ["tests*"]
if sys.version_info[0] == 2:
excluded_packages.append("vcr.stubs.aiohttp_stubs")
setup( setup(
name="vcrpy", name="vcrpy",
version="2.1.1", version=find_version("vcr", "__init__.py"),
description=("Automatically mock your HTTP interactions to simplify and " "speed up testing"), description=("Automatically mock your HTTP interactions to simplify and speed up testing"),
long_description=long_description, long_description=long_description,
long_description_content_type="text/x-rst",
author="Kevin McCarthy", author="Kevin McCarthy",
author_email="me@kevinmccarthy.org", author_email="me@kevinmccarthy.org",
url="https://github.com/kevin1024/vcrpy", url="https://github.com/kevin1024/vcrpy",
packages=find_packages(exclude=excluded_packages), packages=find_packages(exclude=["tests*"]),
python_requires=">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*", python_requires=">=3.5",
install_requires=install_requires, install_requires=install_requires,
license="MIT", license="MIT",
tests_require=["pytest", "mock", "pytest-httpbin"], tests_require=["pytest", "mock", "pytest-httpbin"],
@@ -54,12 +69,12 @@ setup(
"Environment :: Console", "Environment :: Console",
"Intended Audience :: Developers", "Intended Audience :: Developers",
"Programming Language :: Python", "Programming Language :: Python",
"Programming Language :: Python :: 2",
"Programming Language :: Python :: 2.7",
"Programming Language :: Python :: 3", "Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.5",
"Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.6",
"Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3 :: Only",
"Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: CPython",
"Programming Language :: Python :: Implementation :: PyPy", "Programming Language :: Python :: Implementation :: PyPy",
"Topic :: Software Development :: Testing", "Topic :: Software Development :: Testing",

View File

@@ -0,0 +1,41 @@
interactions:
- request:
body: ''
headers:
accept:
- '*/*'
accept-encoding:
- gzip, deflate, br
connection:
- keep-alive
host:
- httpbin.org
user-agent:
- python-httpx/0.12.1
method: GET
uri: https://httpbin.org/headers
response:
content: "{\n \"headers\": {\n \"Accept\": \"*/*\", \n \"Accept-Encoding\"\
: \"gzip, deflate, br\", \n \"Host\": \"httpbin.org\", \n \"User-Agent\"\
: \"python-httpx/0.12.1\", \n \"X-Amzn-Trace-Id\": \"Root=1-5ea778c9-ea76170da792abdbf7614067\"\
\n }\n}\n"
headers:
access-control-allow-credentials:
- 'true'
access-control-allow-origin:
- '*'
connection:
- keep-alive
content-length:
- '226'
content-type:
- application/json
date:
- Tue, 28 Apr 2020 00:28:57 GMT
server:
- gunicorn/19.9.0
via:
- my_own_proxy
http_version: HTTP/1.1
status_code: 200
version: 1

View File

@@ -1,5 +1,6 @@
import contextlib import contextlib
import logging import logging
import urllib.parse
import pytest import pytest
@@ -179,9 +180,8 @@ def test_params_same_url_distinct_params(tmpdir, scheme):
other_params = {"other": "params"} other_params = {"other": "params"}
with vcr.use_cassette(str(tmpdir.join("get.yaml"))) as cassette: with vcr.use_cassette(str(tmpdir.join("get.yaml"))) as cassette:
response, cassette_response_text = get(url, output="text", params=other_params) with pytest.raises(vcr.errors.CannotOverwriteExistingCassetteException):
assert "No match for the request" in cassette_response_text get(url, output="text", params=other_params)
assert response.status == 599
def test_params_on_url(tmpdir, scheme): def test_params_on_url(tmpdir, scheme):
@@ -250,7 +250,7 @@ def test_aiohttp_test_client_json(aiohttp_client, tmpdir):
def test_redirect(aiohttp_client, tmpdir): def test_redirect(aiohttp_client, tmpdir):
url = "https://httpbin.org/redirect/2" url = "https://mockbin.org/redirect/302/2"
with vcr.use_cassette(str(tmpdir.join("redirect.yaml"))): with vcr.use_cassette(str(tmpdir.join("redirect.yaml"))):
response, _ = get(url) response, _ = get(url)
@@ -262,3 +262,140 @@ def test_redirect(aiohttp_client, tmpdir):
assert len(cassette_response.history) == len(response.history) assert len(cassette_response.history) == len(response.history)
assert len(cassette) == 3 assert len(cassette) == 3
assert cassette.play_count == 3 assert cassette.play_count == 3
# Assert that the real response and the cassette response have a similar
# looking request_info.
assert cassette_response.request_info.url == response.request_info.url
assert cassette_response.request_info.method == response.request_info.method
assert {k: v for k, v in cassette_response.request_info.headers.items()} == {
k: v for k, v in response.request_info.headers.items()
}
assert cassette_response.request_info.real_url == response.request_info.real_url
def test_not_modified(aiohttp_client, tmpdir):
"""It doesn't try to redirect on 304"""
url = "https://httpbin.org/status/304"
with vcr.use_cassette(str(tmpdir.join("not_modified.yaml"))):
response, _ = get(url)
with vcr.use_cassette(str(tmpdir.join("not_modified.yaml"))) as cassette:
cassette_response, _ = get(url)
assert cassette_response.status == 304
assert response.status == 304
assert len(cassette_response.history) == len(response.history)
assert len(cassette) == 1
assert cassette.play_count == 1
def test_double_requests(tmpdir):
"""We should capture, record, and replay all requests and response chains,
even if there are duplicate ones.
We should replay in the order we saw them.
"""
url = "https://httpbin.org/get"
with vcr.use_cassette(str(tmpdir.join("text.yaml"))):
_, response_text1 = get(url, output="text")
_, response_text2 = get(url, output="text")
with vcr.use_cassette(str(tmpdir.join("text.yaml"))) as cassette:
resp, cassette_response_text = get(url, output="text")
assert resp.status == 200
assert cassette_response_text == response_text1
# We made only one request, so we should only play 1 recording.
assert cassette.play_count == 1
# Now make the second test to url
resp, cassette_response_text = get(url, output="text")
assert resp.status == 200
assert cassette_response_text == response_text2
# Now that we made both requests, we should have played both.
assert cassette.play_count == 2
def test_cookies(scheme, tmpdir):
async def run(loop):
cookies_url = scheme + (
"://httpbin.org/response-headers?"
"set-cookie=" + urllib.parse.quote("cookie_1=val_1; Path=/") + "&"
"Set-Cookie=" + urllib.parse.quote("Cookie_2=Val_2; Path=/")
)
home_url = scheme + "://httpbin.org/"
tmp = str(tmpdir.join("cookies.yaml"))
req_cookies = {"Cookie_3": "Val_3"}
req_headers = {"Cookie": "Cookie_4=Val_4"}
# ------------------------- Record -------------------------- #
with vcr.use_cassette(tmp) as cassette:
async with aiohttp.ClientSession(loop=loop) as session:
cookies_resp = await session.get(cookies_url)
home_resp = await session.get(home_url, cookies=req_cookies, headers=req_headers)
assert cassette.play_count == 0
assert_responses(cookies_resp, home_resp)
# -------------------------- Play --------------------------- #
with vcr.use_cassette(tmp, record_mode=vcr.mode.NONE) as cassette:
async with aiohttp.ClientSession(loop=loop) as session:
cookies_resp = await session.get(cookies_url)
home_resp = await session.get(home_url, cookies=req_cookies, headers=req_headers)
assert cassette.play_count == 2
assert_responses(cookies_resp, home_resp)
def assert_responses(cookies_resp, home_resp):
assert cookies_resp.cookies.get("cookie_1").value == "val_1"
assert cookies_resp.cookies.get("Cookie_2").value == "Val_2"
request_cookies = home_resp.request_info.headers["cookie"]
assert "cookie_1=val_1" in request_cookies
assert "Cookie_2=Val_2" in request_cookies
assert "Cookie_3=Val_3" in request_cookies
assert "Cookie_4=Val_4" in request_cookies
run_in_loop(run)
def test_cookies_redirect(scheme, tmpdir):
async def run(loop):
# Sets cookie as provided by the query string and redirects
cookies_url = scheme + "://httpbin.org/cookies/set?Cookie_1=Val_1"
tmp = str(tmpdir.join("cookies.yaml"))
# ------------------------- Record -------------------------- #
with vcr.use_cassette(tmp) as cassette:
async with aiohttp.ClientSession(loop=loop) as session:
cookies_resp = await session.get(cookies_url)
assert not cookies_resp.cookies
cookies = session.cookie_jar.filter_cookies(cookies_url)
assert cookies["Cookie_1"].value == "Val_1"
assert cassette.play_count == 0
cassette.requests[1].headers["Cookie"] == "Cookie_1=Val_1"
# -------------------------- Play --------------------------- #
with vcr.use_cassette(tmp, record_mode=vcr.mode.NONE) as cassette:
async with aiohttp.ClientSession(loop=loop) as session:
cookies_resp = await session.get(cookies_url)
assert not cookies_resp.cookies
cookies = session.cookie_jar.filter_cookies(cookies_url)
assert cookies["Cookie_1"].value == "Val_1"
assert cassette.play_count == 2
cassette.requests[1].headers["Cookie"] == "Cookie_1=Val_1"
# Assert that it's ignoring expiration date
with vcr.use_cassette(tmp, record_mode=vcr.mode.NONE) as cassette:
cassette.responses[0]["headers"]["set-cookie"] = [
"Cookie_1=Val_1; Expires=Wed, 21 Oct 2015 07:28:00 GMT"
]
async with aiohttp.ClientSession(loop=loop) as session:
cookies_resp = await session.get(cookies_url)
assert not cookies_resp.cookies
cookies = session.cookie_jar.filter_cookies(cookies_url)
assert cookies["Cookie_1"].value == "Val_1"
run_in_loop(run)

View File

@@ -3,7 +3,7 @@
# External imports # External imports
import os import os
from six.moves.urllib.request import urlopen from urllib.request import urlopen
# Internal imports # Internal imports
import vcr import vcr

View File

@@ -6,14 +6,9 @@ import boto # NOQA
import boto.iam # NOQA import boto.iam # NOQA
from boto.s3.connection import S3Connection # NOQA from boto.s3.connection import S3Connection # NOQA
from boto.s3.key import Key # NOQA from boto.s3.key import Key # NOQA
from configparser import DuplicateSectionError # NOQA
import vcr # NOQA import vcr # NOQA
try: # NOQA
from ConfigParser import DuplicateSectionError # NOQA
except ImportError: # NOQA
# python3
from configparser import DuplicateSectionError # NOQA
def test_boto_stubs(tmpdir): def test_boto_stubs(tmpdir):
with vcr.use_cassette(str(tmpdir.join("boto-stubs.yml"))): with vcr.use_cassette(str(tmpdir.join("boto-stubs.yml"))):

View File

@@ -2,7 +2,7 @@ import os
import json import json
import pytest import pytest
import vcr import vcr
from six.moves.urllib.request import urlopen from urllib.request import urlopen
def test_set_serializer_default_config(tmpdir, httpbin): def test_set_serializer_default_config(tmpdir, httpbin):
@@ -13,7 +13,9 @@ def test_set_serializer_default_config(tmpdir, httpbin):
urlopen(httpbin.url + "/get") urlopen(httpbin.url + "/get")
with open(str(tmpdir.join("test.json"))) as f: with open(str(tmpdir.join("test.json"))) as f:
assert json.loads(f.read()) file_content = f.read()
assert file_content.endswith("\n")
assert json.loads(file_content)
def test_default_set_cassette_library_dir(tmpdir, httpbin): def test_default_set_cassette_library_dir(tmpdir, httpbin):

View File

@@ -4,7 +4,7 @@
# External imports # External imports
import os import os
import time import time
from six.moves.urllib.request import urlopen from urllib.request import urlopen
# Internal imports # Internal imports
import vcr import vcr
@@ -45,7 +45,7 @@ def test_disk_saver_write(tmpdir, httpbin):
# the mtime doesn't change # the mtime doesn't change
time.sleep(1) time.sleep(1)
with vcr.use_cassette(fname, record_mode="any") as cass: with vcr.use_cassette(fname, record_mode=vcr.mode.ANY) as cass:
urlopen(httpbin.url).read() urlopen(httpbin.url).read()
urlopen(httpbin.url + "/get").read() urlopen(httpbin.url + "/get").read()
assert cass.play_count == 1 assert cass.play_count == 1

View File

@@ -1,8 +1,8 @@
import base64 import base64
import pytest import pytest
from six.moves.urllib.request import urlopen, Request from urllib.request import urlopen, Request
from six.moves.urllib.parse import urlencode from urllib.parse import urlencode
from six.moves.urllib.error import HTTPError from urllib.error import HTTPError
import vcr import vcr
import json import json
from assertions import assert_cassette_has_one_response, assert_is_json from assertions import assert_cassette_has_one_response, assert_is_json

View File

@@ -3,7 +3,7 @@
import sys import sys
from six.moves.urllib_parse import urlencode from urllib.parse import urlencode
import pytest import pytest
import pytest_httpbin.certs import pytest_httpbin.certs
@@ -111,7 +111,7 @@ def test_post_data(tmpdir, httpbin_both):
def test_post_unicode_data(tmpdir, httpbin_both): def test_post_unicode_data(tmpdir, httpbin_both):
"""Ensure that it works when posting unicode data""" """Ensure that it works when posting unicode data"""
data = urlencode({"snowman": u"".encode("utf-8")}) data = urlencode({"snowman": "".encode()})
url = httpbin_both.url + "/post" url = httpbin_both.url + "/post"
with vcr.use_cassette(str(tmpdir.join("post_data.yaml"))): with vcr.use_cassette(str(tmpdir.join("post_data.yaml"))):
_, res1 = http().request(url, "POST", data) _, res1 = http().request(url, "POST", data)

View File

@@ -0,0 +1,255 @@
import pytest
import contextlib
import os
asyncio = pytest.importorskip("asyncio")
httpx = pytest.importorskip("httpx")
import vcr # noqa: E402
class BaseDoRequest:
_client_class = None
def __init__(self, *args, **kwargs):
self._client = self._client_class(*args, **kwargs)
class DoSyncRequest(BaseDoRequest):
_client_class = httpx.Client
def __call__(self, *args, **kwargs):
return self._client.request(*args, timeout=60, **kwargs)
class DoAsyncRequest(BaseDoRequest):
_client_class = httpx.AsyncClient
@staticmethod
def run_in_loop(coroutine):
with contextlib.closing(asyncio.new_event_loop()) as loop:
asyncio.set_event_loop(loop)
task = loop.create_task(coroutine)
return loop.run_until_complete(task)
def __call__(self, *args, **kwargs):
async def _request():
async with self._client as c:
return await c.request(*args, **kwargs)
return DoAsyncRequest.run_in_loop(_request())
def pytest_generate_tests(metafunc):
if "do_request" in metafunc.fixturenames:
metafunc.parametrize("do_request", [DoAsyncRequest, DoSyncRequest])
if "scheme" in metafunc.fixturenames:
metafunc.parametrize("scheme", ["http", "https"])
@pytest.fixture
def yml(tmpdir, request):
return str(tmpdir.join(request.function.__name__ + ".yaml"))
def test_status(tmpdir, scheme, do_request):
url = scheme + "://mockbin.org/request"
with vcr.use_cassette(str(tmpdir.join("status.yaml"))):
response = do_request()("GET", url)
with vcr.use_cassette(str(tmpdir.join("status.yaml"))) as cassette:
cassette_response = do_request()("GET", url)
assert cassette_response.status_code == response.status_code
assert cassette.play_count == 1
def test_case_insensitive_headers(tmpdir, scheme, do_request):
url = scheme + "://mockbin.org/request"
with vcr.use_cassette(str(tmpdir.join("whatever.yaml"))):
do_request()("GET", url)
with vcr.use_cassette(str(tmpdir.join("whatever.yaml"))) as cassette:
cassette_response = do_request()("GET", url)
assert "Content-Type" in cassette_response.headers
assert "content-type" in cassette_response.headers
assert cassette.play_count == 1
def test_content(tmpdir, scheme, do_request):
url = scheme + "://httpbin.org"
with vcr.use_cassette(str(tmpdir.join("cointent.yaml"))):
response = do_request()("GET", url)
with vcr.use_cassette(str(tmpdir.join("cointent.yaml"))) as cassette:
cassette_response = do_request()("GET", url)
assert cassette_response.content == response.content
assert cassette.play_count == 1
def test_json(tmpdir, scheme, do_request):
url = scheme + "://httpbin.org/get"
headers = {"Content-Type": "application/json"}
with vcr.use_cassette(str(tmpdir.join("json.yaml"))):
response = do_request(headers=headers)("GET", url)
with vcr.use_cassette(str(tmpdir.join("json.yaml"))) as cassette:
cassette_response = do_request(headers=headers)("GET", url)
assert cassette_response.json() == response.json()
assert cassette.play_count == 1
def test_params_same_url_distinct_params(tmpdir, scheme, do_request):
url = scheme + "://httpbin.org/get"
headers = {"Content-Type": "application/json"}
params = {"a": 1, "b": False, "c": "c"}
with vcr.use_cassette(str(tmpdir.join("get.yaml"))) as cassette:
response = do_request()("GET", url, params=params, headers=headers)
with vcr.use_cassette(str(tmpdir.join("get.yaml"))) as cassette:
cassette_response = do_request()("GET", url, params=params, headers=headers)
assert cassette_response.request.url == response.request.url
assert cassette_response.json() == response.json()
assert cassette.play_count == 1
params = {"other": "params"}
with vcr.use_cassette(str(tmpdir.join("get.yaml"))) as cassette:
with pytest.raises(vcr.errors.CannotOverwriteExistingCassetteException):
do_request()("GET", url, params=params, headers=headers)
def test_redirect(tmpdir, do_request, yml):
url = "https://mockbin.org/redirect/303/2"
response = do_request()("GET", url)
with vcr.use_cassette(yml):
response = do_request()("GET", url)
with vcr.use_cassette(yml) as cassette:
cassette_response = do_request()("GET", url)
assert cassette_response.status_code == response.status_code
assert len(cassette_response.history) == len(response.history)
assert len(cassette) == 3
assert cassette.play_count == 3
# Assert that the real response and the cassette response have a similar
# looking request_info.
assert cassette_response.request.url == response.request.url
assert cassette_response.request.method == response.request.method
assert {k: v for k, v in cassette_response.request.headers.items()} == {
k: v for k, v in response.request.headers.items()
}
def test_work_with_gzipped_data(tmpdir, do_request, yml):
with vcr.use_cassette(yml):
do_request()("GET", "https://httpbin.org/gzip")
with vcr.use_cassette(yml) as cassette:
cassette_response = do_request()("GET", "https://httpbin.org/gzip")
assert "gzip" in cassette_response.json()["headers"]["Accept-Encoding"]
assert cassette_response.read()
assert cassette.play_count == 1
@pytest.mark.parametrize("url", ["https://github.com/kevin1024/vcrpy/issues/" + str(i) for i in range(3, 6)])
def test_simple_fetching(tmpdir, do_request, yml, url):
with vcr.use_cassette(yml):
do_request()("GET", url)
with vcr.use_cassette(yml) as cassette:
cassette_response = do_request()("GET", url)
assert str(cassette_response.request.url) == url
assert cassette.play_count == 1
def test_behind_proxy(do_request):
# This is recorded because otherwise we should have a live proxy somewhere.
yml = (
os.path.dirname(os.path.realpath(__file__)) + "/cassettes/" + "test_httpx_test_test_behind_proxy.yml"
)
url = "https://httpbin.org/headers"
proxy = "http://localhost:8080"
proxies = {"http": proxy, "https": proxy}
with vcr.use_cassette(yml):
response = do_request(proxies=proxies, verify=False)("GET", url)
with vcr.use_cassette(yml) as cassette:
cassette_response = do_request(proxies=proxies, verify=False)("GET", url)
assert str(cassette_response.request.url) == url
assert cassette.play_count == 1
assert cassette_response.headers["Via"] == "my_own_proxy", str(cassette_response.headers)
assert cassette_response.request.url == response.request.url
def test_cookies(tmpdir, scheme, do_request):
def client_cookies(client):
return [c for c in client._client.cookies]
def response_cookies(response):
return [c for c in response.cookies]
client = do_request()
assert client_cookies(client) == []
url = scheme + "://httpbin.org"
testfile = str(tmpdir.join("cookies.yml"))
with vcr.use_cassette(testfile):
r1 = client("GET", url + "/cookies/set?k1=v1&k2=v2")
assert response_cookies(r1.history[0]) == ["k1", "k2"]
assert response_cookies(r1) == []
r2 = client("GET", url + "/cookies")
assert len(r2.json()["cookies"]) == 2
assert client_cookies(client) == ["k1", "k2"]
new_client = do_request()
assert client_cookies(new_client) == []
with vcr.use_cassette(testfile) as cassette:
cassette_response = new_client("GET", url + "/cookies/set?k1=v1&k2=v2")
assert response_cookies(cassette_response.history[0]) == ["k1", "k2"]
assert response_cookies(cassette_response) == []
assert cassette.play_count == 2
assert client_cookies(new_client) == ["k1", "k2"]
def test_relative_redirects(tmpdir, scheme, do_request):
url = scheme + "://mockbin.com/redirect/301?to=/redirect/301?to=/request"
testfile = str(tmpdir.join("relative_redirects.yml"))
with vcr.use_cassette(testfile):
response = do_request()("GET", url)
assert len(response.history) == 2, response
assert response.json()["url"].endswith("request")
with vcr.use_cassette(testfile) as cassette:
response = do_request()("GET", url)
assert len(response.history) == 2
assert response.json()["url"].endswith("request")
assert cassette.play_count == 3
def test_redirect_wo_allow_redirects(do_request, yml):
url = "https://mockbin.org/redirect/308/5"
with vcr.use_cassette(yml):
response = do_request()("GET", url, allow_redirects=False)
assert str(response.url).endswith("308/5")
assert response.status_code == 308
with vcr.use_cassette(yml) as cassette:
response = do_request()("GET", url, allow_redirects=False)
assert str(response.url).endswith("308/5")
assert response.status_code == 308
assert cassette.play_count == 1

View File

@@ -1,4 +1,4 @@
from six.moves.urllib.request import urlopen from urllib.request import urlopen
import socket import socket
from contextlib import contextmanager from contextlib import contextmanager
import vcr import vcr

View File

@@ -1,6 +1,6 @@
import vcr import vcr
import pytest import pytest
from six.moves.urllib.request import urlopen from urllib.request import urlopen
DEFAULT_URI = "http://httpbin.org/get?p1=q1&p2=q2" # base uri for testing DEFAULT_URI = "http://httpbin.org/get?p1=q1&p2=q2" # base uri for testing
@@ -13,13 +13,13 @@ def _replace_httpbin(uri, httpbin, httpbin_secure):
@pytest.fixture @pytest.fixture
def cassette(tmpdir, httpbin, httpbin_secure): def cassette(tmpdir, httpbin, httpbin_secure):
""" """
Helper fixture used to prepare the cassete Helper fixture used to prepare the cassette
returns path to the recorded cassette returns path to the recorded cassette
""" """
default_uri = _replace_httpbin(DEFAULT_URI, httpbin, httpbin_secure) default_uri = _replace_httpbin(DEFAULT_URI, httpbin, httpbin_secure)
cassette_path = str(tmpdir.join("test.yml")) cassette_path = str(tmpdir.join("test.yml"))
with vcr.use_cassette(cassette_path, record_mode="all"): with vcr.use_cassette(cassette_path, record_mode=vcr.mode.ALL):
urlopen(default_uri) urlopen(default_uri)
return cassette_path return cassette_path

View File

@@ -1,6 +1,6 @@
import pytest import pytest
import vcr import vcr
from six.moves.urllib.request import urlopen from urllib.request import urlopen
def test_making_extra_request_raises_exception(tmpdir, httpbin): def test_making_extra_request_raises_exception(tmpdir, httpbin):

View File

@@ -5,8 +5,9 @@
import multiprocessing import multiprocessing
import pytest import pytest
from six.moves import socketserver, SimpleHTTPServer import http.server
from six.moves.urllib.request import urlopen import socketserver
from urllib.request import urlopen
# Internal imports # Internal imports
import vcr import vcr
@@ -15,7 +16,7 @@ import vcr
requests = pytest.importorskip("requests") requests = pytest.importorskip("requests")
class Proxy(SimpleHTTPServer.SimpleHTTPRequestHandler): class Proxy(http.server.SimpleHTTPRequestHandler):
""" """
Simple proxy server. Simple proxy server.

View File

@@ -1,15 +1,15 @@
import pytest import pytest
import vcr import vcr
from six.moves.urllib.request import urlopen from urllib.request import urlopen
def test_once_record_mode(tmpdir, httpbin): def test_once_record_mode(tmpdir, httpbin):
testfile = str(tmpdir.join("recordmode.yml")) testfile = str(tmpdir.join("recordmode.yml"))
with vcr.use_cassette(testfile, record_mode="once"): with vcr.use_cassette(testfile, record_mode=vcr.mode.ONCE):
# cassette file doesn't exist, so create. # cassette file doesn't exist, so create.
urlopen(httpbin.url).read() urlopen(httpbin.url).read()
with vcr.use_cassette(testfile, record_mode="once"): with vcr.use_cassette(testfile, record_mode=vcr.mode.ONCE):
# make the same request again # make the same request again
urlopen(httpbin.url).read() urlopen(httpbin.url).read()
@@ -22,12 +22,12 @@ def test_once_record_mode(tmpdir, httpbin):
def test_once_record_mode_two_times(tmpdir, httpbin): def test_once_record_mode_two_times(tmpdir, httpbin):
testfile = str(tmpdir.join("recordmode.yml")) testfile = str(tmpdir.join("recordmode.yml"))
with vcr.use_cassette(testfile, record_mode="once"): with vcr.use_cassette(testfile, record_mode=vcr.mode.ONCE):
# get two of the same file # get two of the same file
urlopen(httpbin.url).read() urlopen(httpbin.url).read()
urlopen(httpbin.url).read() urlopen(httpbin.url).read()
with vcr.use_cassette(testfile, record_mode="once"): with vcr.use_cassette(testfile, record_mode=vcr.mode.ONCE):
# do it again # do it again
urlopen(httpbin.url).read() urlopen(httpbin.url).read()
urlopen(httpbin.url).read() urlopen(httpbin.url).read()
@@ -35,7 +35,7 @@ def test_once_record_mode_two_times(tmpdir, httpbin):
def test_once_mode_three_times(tmpdir, httpbin): def test_once_mode_three_times(tmpdir, httpbin):
testfile = str(tmpdir.join("recordmode.yml")) testfile = str(tmpdir.join("recordmode.yml"))
with vcr.use_cassette(testfile, record_mode="once"): with vcr.use_cassette(testfile, record_mode=vcr.mode.ONCE):
# get three of the same file # get three of the same file
urlopen(httpbin.url).read() urlopen(httpbin.url).read()
urlopen(httpbin.url).read() urlopen(httpbin.url).read()
@@ -45,11 +45,11 @@ def test_once_mode_three_times(tmpdir, httpbin):
def test_new_episodes_record_mode(tmpdir, httpbin): def test_new_episodes_record_mode(tmpdir, httpbin):
testfile = str(tmpdir.join("recordmode.yml")) testfile = str(tmpdir.join("recordmode.yml"))
with vcr.use_cassette(testfile, record_mode="new_episodes"): with vcr.use_cassette(testfile, record_mode=vcr.mode.NEW_EPISODES):
# cassette file doesn't exist, so create. # cassette file doesn't exist, so create.
urlopen(httpbin.url).read() urlopen(httpbin.url).read()
with vcr.use_cassette(testfile, record_mode="new_episodes") as cass: with vcr.use_cassette(testfile, record_mode=vcr.mode.NEW_EPISODES) as cass:
# make the same request again # make the same request again
urlopen(httpbin.url).read() urlopen(httpbin.url).read()
@@ -66,7 +66,7 @@ def test_new_episodes_record_mode(tmpdir, httpbin):
# not all responses have been played # not all responses have been played
assert not cass.all_played assert not cass.all_played
with vcr.use_cassette(testfile, record_mode="new_episodes") as cass: with vcr.use_cassette(testfile, record_mode=vcr.mode.NEW_EPISODES) as cass:
# the cassette should now have 2 responses # the cassette should now have 2 responses
assert len(cass.responses) == 2 assert len(cass.responses) == 2
@@ -74,11 +74,11 @@ def test_new_episodes_record_mode(tmpdir, httpbin):
def test_new_episodes_record_mode_two_times(tmpdir, httpbin): def test_new_episodes_record_mode_two_times(tmpdir, httpbin):
testfile = str(tmpdir.join("recordmode.yml")) testfile = str(tmpdir.join("recordmode.yml"))
url = httpbin.url + "/bytes/1024" url = httpbin.url + "/bytes/1024"
with vcr.use_cassette(testfile, record_mode="new_episodes"): with vcr.use_cassette(testfile, record_mode=vcr.mode.NEW_EPISODES):
# cassette file doesn't exist, so create. # cassette file doesn't exist, so create.
original_first_response = urlopen(url).read() original_first_response = urlopen(url).read()
with vcr.use_cassette(testfile, record_mode="new_episodes"): with vcr.use_cassette(testfile, record_mode=vcr.mode.NEW_EPISODES):
# make the same request again # make the same request again
assert urlopen(url).read() == original_first_response assert urlopen(url).read() == original_first_response
@@ -86,7 +86,7 @@ def test_new_episodes_record_mode_two_times(tmpdir, httpbin):
# to the cassette without repercussions # to the cassette without repercussions
original_second_response = urlopen(url).read() original_second_response = urlopen(url).read()
with vcr.use_cassette(testfile, record_mode="once"): with vcr.use_cassette(testfile, record_mode=vcr.mode.ONCE):
# make the same request again # make the same request again
assert urlopen(url).read() == original_first_response assert urlopen(url).read() == original_first_response
assert urlopen(url).read() == original_second_response assert urlopen(url).read() == original_second_response
@@ -99,11 +99,11 @@ def test_new_episodes_record_mode_two_times(tmpdir, httpbin):
def test_all_record_mode(tmpdir, httpbin): def test_all_record_mode(tmpdir, httpbin):
testfile = str(tmpdir.join("recordmode.yml")) testfile = str(tmpdir.join("recordmode.yml"))
with vcr.use_cassette(testfile, record_mode="all"): with vcr.use_cassette(testfile, record_mode=vcr.mode.ALL):
# cassette file doesn't exist, so create. # cassette file doesn't exist, so create.
urlopen(httpbin.url).read() urlopen(httpbin.url).read()
with vcr.use_cassette(testfile, record_mode="all") as cass: with vcr.use_cassette(testfile, record_mode=vcr.mode.ALL) as cass:
# make the same request again # make the same request again
urlopen(httpbin.url).read() urlopen(httpbin.url).read()
@@ -121,7 +121,7 @@ def test_none_record_mode(tmpdir, httpbin):
# Cassette file doesn't exist, yet we are trying to make a request. # Cassette file doesn't exist, yet we are trying to make a request.
# raise hell. # raise hell.
testfile = str(tmpdir.join("recordmode.yml")) testfile = str(tmpdir.join("recordmode.yml"))
with vcr.use_cassette(testfile, record_mode="none"): with vcr.use_cassette(testfile, record_mode=vcr.mode.NONE):
with pytest.raises(Exception): with pytest.raises(Exception):
urlopen(httpbin.url).read() urlopen(httpbin.url).read()
@@ -130,11 +130,11 @@ def test_none_record_mode_with_existing_cassette(tmpdir, httpbin):
# create a cassette file # create a cassette file
testfile = str(tmpdir.join("recordmode.yml")) testfile = str(tmpdir.join("recordmode.yml"))
with vcr.use_cassette(testfile, record_mode="all"): with vcr.use_cassette(testfile, record_mode=vcr.mode.ALL):
urlopen(httpbin.url).read() urlopen(httpbin.url).read()
# play from cassette file # play from cassette file
with vcr.use_cassette(testfile, record_mode="none") as cass: with vcr.use_cassette(testfile, record_mode=vcr.mode.NONE) as cass:
urlopen(httpbin.url).read() urlopen(httpbin.url).read()
assert cass.play_count == 1 assert cass.play_count == 1
# but if I try to hit the net, raise an exception. # but if I try to hit the net, raise an exception.

View File

@@ -1,5 +1,5 @@
import vcr import vcr
from six.moves.urllib.request import urlopen from urllib.request import urlopen
def true_matcher(r1, r2): def true_matcher(r1, r2):

View File

@@ -3,7 +3,7 @@
# External imports # External imports
import os import os
from six.moves.urllib.request import urlopen from urllib.request import urlopen
# Internal imports # Internal imports
import vcr import vcr
@@ -12,7 +12,7 @@ from vcr.persisters.filesystem import FilesystemPersister
class CustomFilesystemPersister(object): class CustomFilesystemPersister(object):
"""Behaves just like default FilesystemPersister but adds .test extension """Behaves just like default FilesystemPersister but adds .test extension
to the cassette file""" to the cassette file"""
@staticmethod @staticmethod
def load_cassette(cassette_path, serializer): def load_cassette(cassette_path, serializer):

View File

@@ -1,7 +1,7 @@
import vcr import vcr
class MockSerializer(object): class MockSerializer:
def __init__(self): def __init__(self):
self.serialize_count = 0 self.serialize_count = 0
self.deserialize_count = 0 self.deserialize_count = 0

View File

@@ -1,5 +1,5 @@
import vcr import vcr
from six.moves.urllib.request import urlopen from urllib.request import urlopen
def test_recorded_request_uri_with_redirected_request(tmpdir, httpbin): def test_recorded_request_uri_with_redirected_request(tmpdir, httpbin):

View File

@@ -1,7 +1,7 @@
import vcr import vcr
import zlib import zlib
import json import json
import six.moves.http_client as httplib import http.client as httplib
from assertions import assert_is_json from assertions import assert_is_json

View File

@@ -90,10 +90,10 @@ def test_body(get_client, tmpdir, scheme):
@pytest.mark.gen_test @pytest.mark.gen_test
def test_effective_url(get_client, scheme, tmpdir): def test_effective_url(get_client, scheme, tmpdir):
"""Ensure that the effective_url is captured""" """Ensure that the effective_url is captured"""
url = scheme + "://httpbin.org/redirect-to?url=/html" url = scheme + "://mockbin.org/redirect/301?url=/html"
with vcr.use_cassette(str(tmpdir.join("url.yaml"))): with vcr.use_cassette(str(tmpdir.join("url.yaml"))):
effective_url = (yield get(get_client(), url)).effective_url effective_url = (yield get(get_client(), url)).effective_url
assert effective_url == scheme + "://httpbin.org/html" assert effective_url == scheme + "://mockbin.org/redirect/301/0"
with vcr.use_cassette(str(tmpdir.join("url.yaml"))) as cass: with vcr.use_cassette(str(tmpdir.join("url.yaml"))) as cass:
assert effective_url == (yield get(get_client(), url)).effective_url assert effective_url == (yield get(get_client(), url)).effective_url
@@ -156,7 +156,7 @@ def test_post(get_client, tmpdir, scheme):
@pytest.mark.gen_test @pytest.mark.gen_test
def test_redirects(get_client, tmpdir, scheme): def test_redirects(get_client, tmpdir, scheme):
"""Ensure that we can handle redirects""" """Ensure that we can handle redirects"""
url = scheme + "://httpbin.org/redirect-to?url=bytes/1024" url = scheme + "://mockbin.org/redirect/301?url=bytes/1024"
with vcr.use_cassette(str(tmpdir.join("requests.yaml"))): with vcr.use_cassette(str(tmpdir.join("requests.yaml"))):
content = (yield get(get_client(), url)).body content = (yield get(get_client(), url)).body

View File

@@ -2,8 +2,8 @@
"""Integration tests with urllib2""" """Integration tests with urllib2"""
import ssl import ssl
from six.moves.urllib.request import urlopen from urllib.request import urlopen
from six.moves.urllib_parse import urlencode from urllib.parse import urlencode
import pytest_httpbin.certs import pytest_httpbin.certs
# Internal imports # Internal imports
@@ -104,7 +104,7 @@ def test_post_data(httpbin_both, tmpdir):
def test_post_unicode_data(httpbin_both, tmpdir): def test_post_unicode_data(httpbin_both, tmpdir):
"""Ensure that it works when posting unicode data""" """Ensure that it works when posting unicode data"""
data = urlencode({"snowman": u"".encode("utf-8")}).encode("utf-8") data = urlencode({"snowman": "".encode()}).encode("utf-8")
url = httpbin_both.url + "/post" url = httpbin_both.url + "/post"
with vcr.use_cassette(str(tmpdir.join("post_data.yaml"))): with vcr.use_cassette(str(tmpdir.join("post_data.yaml"))):
res1 = urlopen_with_cafile(url, data).read() res1 = urlopen_with_cafile(url, data).read()

View File

@@ -1,16 +1,13 @@
import http.client as httplib
import multiprocessing import multiprocessing
import pytest import pytest
from six.moves import xmlrpc_client, xmlrpc_server from xmlrpc.client import ServerProxy
from xmlrpc.server import SimpleXMLRPCServer
requests = pytest.importorskip("requests") requests = pytest.importorskip("requests")
import vcr # NOQA import vcr # NOQA
try:
import httplib
except ImportError:
import http.client as httplib
def test_domain_redirect(): def test_domain_redirect():
"""Ensure that redirects across domains are considered unique""" """Ensure that redirects across domains are considered unique"""
@@ -80,7 +77,7 @@ def test_amazon_doctype(tmpdir):
def start_rpc_server(q): def start_rpc_server(q):
httpd = xmlrpc_server.SimpleXMLRPCServer(("127.0.0.1", 0)) httpd = SimpleXMLRPCServer(("127.0.0.1", 0))
httpd.register_function(pow) httpd.register_function(pow)
q.put("http://{}:{}".format(*httpd.server_address)) q.put("http://{}:{}".format(*httpd.server_address))
httpd.serve_forever() httpd.serve_forever()
@@ -99,11 +96,11 @@ def rpc_server():
def test_xmlrpclib(tmpdir, rpc_server): def test_xmlrpclib(tmpdir, rpc_server):
with vcr.use_cassette(str(tmpdir.join("xmlrpcvideo.yaml"))): with vcr.use_cassette(str(tmpdir.join("xmlrpcvideo.yaml"))):
roundup_server = xmlrpc_client.ServerProxy(rpc_server, allow_none=True) roundup_server = ServerProxy(rpc_server, allow_none=True)
original_schema = roundup_server.pow(2, 4) original_schema = roundup_server.pow(2, 4)
with vcr.use_cassette(str(tmpdir.join("xmlrpcvideo.yaml"))): with vcr.use_cassette(str(tmpdir.join("xmlrpcvideo.yaml"))):
roundup_server = xmlrpc_client.ServerProxy(rpc_server, allow_none=True) roundup_server = ServerProxy(rpc_server, allow_none=True)
second_schema = roundup_server.pow(2, 4) second_schema = roundup_server.pow(2, 4)
assert original_schema == second_schema assert original_schema == second_schema

View File

@@ -1,12 +1,12 @@
import contextlib
import copy import copy
import http.client as httplib
import inspect import inspect
import os import os
from unittest import mock
from six.moves import http_client as httplib
import pytest import pytest
import yaml import yaml
from vcr.compat import mock, contextlib
from vcr.cassette import Cassette from vcr.cassette import Cassette
from vcr.errors import UnhandledHTTPRequestError from vcr.errors import UnhandledHTTPRequestError
from vcr.patch import force_reset from vcr.patch import force_reset
@@ -137,6 +137,31 @@ def test_cassette_all_played():
assert a.all_played assert a.all_played
@mock.patch("vcr.cassette.requests_match", _mock_requests_match)
def test_cassette_allow_playback_repeats():
a = Cassette("test", allow_playback_repeats=True)
a.append("foo", "bar")
a.append("other", "resp")
for x in range(10):
assert a.play_response("foo") == "bar"
assert a.play_count == 10
assert a.all_played is False
assert a.play_response("other") == "resp"
assert a.play_count == 11
assert a.all_played
a.allow_playback_repeats = False
with pytest.raises(UnhandledHTTPRequestError) as e:
a.play_response("foo")
assert str(e.value) == "\"The cassette ('test') doesn't contain the request ('foo') asked for\""
a.rewind()
assert a.all_played is False
assert a.play_response("foo") == "bar"
assert a.all_played is False
assert a.play_response("other") == "resp"
assert a.all_played
@mock.patch("vcr.cassette.requests_match", _mock_requests_match) @mock.patch("vcr.cassette.requests_match", _mock_requests_match)
def test_cassette_rewound(): def test_cassette_rewound():
a = Cassette("test") a = Cassette("test")
@@ -208,7 +233,7 @@ def test_nesting_context_managers_by_checking_references_of_http_connection():
def test_custom_patchers(): def test_custom_patchers():
class Test(object): class Test:
attribute = None attribute = None
with Cassette.use(path="custom_patches", custom_patches=((Test, "attribute", VCRHTTPSConnection),)): with Cassette.use(path="custom_patches", custom_patches=((Test, "attribute", VCRHTTPSConnection),)):

View File

@@ -1,6 +1,7 @@
from unittest import mock
import pytest import pytest
from vcr.compat import mock
from vcr import errors from vcr import errors
from vcr.cassette import Cassette from vcr.cassette import Cassette

View File

@@ -1,4 +1,4 @@
from six import BytesIO from io import BytesIO
from vcr.filters import ( from vcr.filters import (
remove_headers, remove_headers,
replace_headers, replace_headers,
@@ -8,10 +8,10 @@ from vcr.filters import (
replace_post_data_parameters, replace_post_data_parameters,
decode_response, decode_response,
) )
from vcr.compat import mock
from vcr.request import Request from vcr.request import Request
import gzip import gzip
import json import json
from unittest import mock
import zlib import zlib

View File

@@ -1,5 +1,5 @@
import itertools import itertools
from vcr.compat import mock from unittest import mock
import pytest import pytest

View File

@@ -1,8 +1,5 @@
# coding: UTF-8 # coding: UTF-8
import io import io
import unittest
import six
from vcr.stubs import VCRHTTPResponse from vcr.stubs import VCRHTTPResponse
@@ -58,7 +55,6 @@ def test_response_headers_should_have_correct_values():
assert response.headers.get("date") == "Fri, 24 Oct 2014 18:35:37 GMT" 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(): def test_response_parses_correctly_and_fp_attribute_error_is_not_thrown():
""" """
Regression test for https://github.com/kevin1024/vcrpy/issues/440 Regression test for https://github.com/kevin1024/vcrpy/issues/440

View File

@@ -1,7 +1,8 @@
# -*- encoding: utf-8 -*- # -*- encoding: utf-8 -*-
from unittest import mock
import pytest import pytest
from vcr.compat import mock
from vcr.request import Request from vcr.request import Request
from vcr.serialize import deserialize, serialize from vcr.serialize import deserialize, serialize
from vcr.serializers import yamlserializer, jsonserializer, compat from vcr.serializers import yamlserializer, jsonserializer, compat
@@ -29,7 +30,7 @@ def test_deserialize_new_json_cassette():
deserialize(f.read(), jsonserializer) deserialize(f.read(), jsonserializer)
REQBODY_TEMPLATE = u"""\ REQBODY_TEMPLATE = """\
interactions: interactions:
- request: - request:
body: {req_body} body: {req_body}

View File

@@ -1,9 +1,11 @@
from unittest import mock
from vcr import mode
from vcr.stubs import VCRHTTPSConnection from vcr.stubs import VCRHTTPSConnection
from vcr.compat import mock
from vcr.cassette import Cassette from vcr.cassette import Cassette
class TestVCRConnection(object): class TestVCRConnection:
def test_setting_of_attributes_get_propogated_to_real_connection(self): def test_setting_of_attributes_get_propogated_to_real_connection(self):
vcr_connection = VCRHTTPSConnection("www.examplehost.com") vcr_connection = VCRHTTPSConnection("www.examplehost.com")
vcr_connection.ssl_version = "example_ssl_version" vcr_connection.ssl_version = "example_ssl_version"
@@ -12,6 +14,6 @@ class TestVCRConnection(object):
@mock.patch("vcr.cassette.Cassette.can_play_response_for", return_value=False) @mock.patch("vcr.cassette.Cassette.can_play_response_for", return_value=False)
def testing_connect(*args): def testing_connect(*args):
vcr_connection = VCRHTTPSConnection("www.google.com") vcr_connection = VCRHTTPSConnection("www.google.com")
vcr_connection.cassette = Cassette("test", record_mode="all") vcr_connection.cassette = Cassette("test", record_mode=mode.ALL)
vcr_connection.real_connection.connect() vcr_connection.real_connection.connect()
assert vcr_connection.real_connection.sock is not None assert vcr_connection.real_connection.sock is not None

View File

@@ -1,10 +1,10 @@
from unittest import mock
import os import os
import pytest import pytest
from six.moves import http_client as httplib import http.client as httplib
from vcr import VCR, use_cassette from vcr import VCR, mode, use_cassette
from vcr.compat import mock
from vcr.request import Request from vcr.request import Request
from vcr.stubs import VCRHTTPSConnection from vcr.stubs import VCRHTTPSConnection
from vcr.patch import _HTTPConnection, force_reset from vcr.patch import _HTTPConnection, force_reset
@@ -170,7 +170,7 @@ def test_fixtures_with_use_cassette(random_fixture):
def test_custom_patchers(): def test_custom_patchers():
class Test(object): class Test:
attribute = None attribute = None
attribute2 = None attribute2 = None
@@ -188,11 +188,11 @@ def test_custom_patchers():
def test_inject_cassette(): def test_inject_cassette():
vcr = VCR(inject_cassette=True) vcr = VCR(inject_cassette=True)
@vcr.use_cassette("test", record_mode="once") @vcr.use_cassette("test", record_mode=mode.ONCE)
def with_cassette_injected(cassette): def with_cassette_injected(cassette):
assert cassette.record_mode == "once" assert cassette.record_mode == mode.ONCE
@vcr.use_cassette("test", record_mode="once", inject_cassette=False) @vcr.use_cassette("test", record_mode=mode.ONCE, inject_cassette=False)
def without_cassette_injected(): def without_cassette_injected():
pass pass
@@ -201,7 +201,7 @@ def test_inject_cassette():
def test_with_current_defaults(): def test_with_current_defaults():
vcr = VCR(inject_cassette=True, record_mode="once") vcr = VCR(inject_cassette=True, record_mode=mode.ONCE)
@vcr.use_cassette("test", with_current_defaults=False) @vcr.use_cassette("test", with_current_defaults=False)
def changing_defaults(cassette, checks): def changing_defaults(cassette, checks):
@@ -212,10 +212,10 @@ def test_with_current_defaults():
checks(cassette) checks(cassette)
def assert_record_mode_once(cassette): def assert_record_mode_once(cassette):
assert cassette.record_mode == "once" assert cassette.record_mode == mode.ONCE
def assert_record_mode_all(cassette): def assert_record_mode_all(cassette):
assert cassette.record_mode == "all" assert cassette.record_mode == mode.ALL
changing_defaults(assert_record_mode_once) changing_defaults(assert_record_mode_once)
current_defaults(assert_record_mode_once) current_defaults(assert_record_mode_once)

40
tox.ini
View File

@@ -3,8 +3,9 @@ skip_missing_interpreters=true
envlist = envlist =
cov-clean, cov-clean,
lint, lint,
{py27,py35,py36,py37,py38,pypy,pypy3}-{requests,httplib2,urllib3,tornado4,boto3}, {py35,py36,py37,py38}-{requests,httplib2,urllib3,tornado4,boto3,aiohttp},
{py35,py36,py37,py38}-{aiohttp}, {py36,py37,py38}-{httpx}
{pypy3}-{requests,httplib2,urllib3,tornado4,boto3},
cov-report cov-report
@@ -34,6 +35,27 @@ deps =
flake8 flake8
black black
[testenv:docs]
# Running sphinx from inside the "docs" directory
# ensures it will not pick up any stray files that might
# get into a virtual environment under the top-level directory
# or other artifacts under build/
changedir = docs
# The only dependency is sphinx
# If we were using extensions packaged separately,
# we would specify them here.
# A better practice is to specify a specific version of sphinx.
deps =
sphinx
sphinx_rtd_theme
# This is the sphinx command to generate HTML.
# In other circumstances, we might want to generate a PDF or an ebook
commands =
sphinx-build -W -b html -d {envtmpdir}/doctrees . {envtmpdir}/html
# We use Python 3.7. Tox sometimes tries to autodetect it based on the name of
# the testenv, but "docs" does not give useful clues so we have to be explicit.
basepython = python3.7
[testenv] [testenv]
# Need to use develop install so that paths # Need to use develop install so that paths
# for aggregate code coverage combine # for aggregate code coverage combine
@@ -42,7 +64,6 @@ commands =
./runtests.sh --cov=./vcr --cov-branch --cov-report=xml --cov-append {posargs} ./runtests.sh --cov=./vcr --cov-branch --cov-report=xml --cov-append {posargs}
deps = deps =
Flask Flask
mock
pytest pytest
pytest-httpbin pytest-httpbin
pytest-cov pytest-cov
@@ -51,17 +72,20 @@ deps =
requests: requests>=2.22.0 requests: requests>=2.22.0
httplib2: httplib2 httplib2: httplib2
urllib3: urllib3 urllib3: urllib3
{py27,py35,py36,pypy}-tornado4: tornado>=4,<5 {py35,py36}-tornado4: tornado>=4,<5
{py27,py35,py36,pypy}-tornado4: pytest-tornado {py35,py36}-tornado4: pytest-tornado
{py27,py35,py36}-tornado4: pycurl {py35,py36}-tornado4: pycurl
boto3: boto3 boto3: boto3
boto3: urllib3 boto3: urllib3
aiohttp: aiohttp aiohttp: aiohttp
aiohttp: pytest-asyncio aiohttp: pytest-asyncio
aiohttp: pytest-aiohttp aiohttp: pytest-aiohttp
httpx: httpx
{py36,py37,py38}-{httpx}: httpx
{py36,py37,py38}-{httpx}: pytest-asyncio
depends = depends =
{py27,py35,py36,py37,pypy}-{lint,requests,httplib2,urllib3,tornado4,boto3},{py35,py36,py37}-{aiohttp}: cov-clean lint,{py35,py36,py37,py38,pypy3}-{requests,httplib2,urllib3,tornado4,boto3},{py35,py36,py37,py38}-{aiohttp},{py36,py37,py38}-{httpx}: cov-clean
cov-report: {py27,py35,py36,py37,pypy}-{lint,requests,httplib2,urllib3,tornado4,boto3},{py35,py36,py37}-{aiohttp} cov-report: lint,{py35,py36,py37,py38,pypy3}-{requests,httplib2,urllib3,tornado4,boto3},{py35,py36,py37,py38}-{aiohttp}
passenv = passenv =
AWS_ACCESS_KEY_ID AWS_ACCESS_KEY_ID
AWS_DEFAULT_REGION AWS_DEFAULT_REGION

BIN
vcr.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 240 KiB

After

Width:  |  Height:  |  Size: 114 KiB

View File

@@ -1,23 +1,9 @@
import logging import logging
import warnings
import sys
from .config import VCR from .config import VCR
from logging import NullHandler
from .record_mode import RecordMode as mode # noqa import is not used in this file
# Set default logging handler to avoid "No handler found" warnings. __version__ = "4.1.1"
try: # Python 2.7+
from logging import NullHandler
except ImportError:
class NullHandler(logging.Handler):
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()) logging.getLogger(__name__).addHandler(NullHandler())

View File

@@ -1,4 +1,5 @@
import collections import collections
import contextlib
import copy import copy
import sys import sys
import inspect import inspect
@@ -6,13 +7,14 @@ import logging
import wrapt import wrapt
from .compat import contextlib
from .errors import UnhandledHTTPRequestError from .errors import UnhandledHTTPRequestError
from .matchers import requests_match, uri, method, get_matchers_results from .matchers import requests_match, uri, method, get_matchers_results
from .patch import CassettePatcherBuilder from .patch import CassettePatcherBuilder
from .serializers import yamlserializer from .serializers import yamlserializer
from .persisters.filesystem import FilesystemPersister from .persisters.filesystem import FilesystemPersister
from .util import partition_dict from .util import partition_dict
from ._handle_coroutine import handle_coroutine
from .record_mode import RecordMode
try: try:
from asyncio import iscoroutinefunction from asyncio import iscoroutinefunction
@@ -22,18 +24,10 @@ except ImportError:
return False 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")
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
class CassetteContextDecorator(object): class CassetteContextDecorator:
"""Context manager/decorator that handles installing the cassette and """Context manager/decorator that handles installing the cassette and
removing cassettes. removing cassettes.
@@ -159,7 +153,7 @@ class CassetteContextDecorator(object):
return new_args_getter return new_args_getter
class Cassette(object): class Cassette:
"""A container for recorded requests and responses""" """A container for recorded requests and responses"""
@classmethod @classmethod
@@ -182,12 +176,13 @@ class Cassette(object):
path, path,
serializer=None, serializer=None,
persister=None, persister=None,
record_mode="once", record_mode=RecordMode.ONCE,
match_on=(uri, method), match_on=(uri, method),
before_record_request=None, before_record_request=None,
before_record_response=None, before_record_response=None,
custom_patches=(), custom_patches=(),
inject=False, inject=False,
allow_playback_repeats=False,
): ):
self._persister = persister or FilesystemPersister self._persister = persister or FilesystemPersister
self._path = path self._path = path
@@ -199,6 +194,7 @@ class Cassette(object):
self.inject = inject self.inject = inject
self.record_mode = record_mode self.record_mode = record_mode
self.custom_patches = custom_patches self.custom_patches = custom_patches
self.allow_playback_repeats = allow_playback_repeats
# self.data is the list of (req, resp) tuples # self.data is the list of (req, resp) tuples
self.data = [] self.data = []
@@ -213,7 +209,7 @@ class Cassette(object):
@property @property
def all_played(self): def all_played(self):
"""Returns True if all responses have been played, False otherwise.""" """Returns True if all responses have been played, False otherwise."""
return self.play_count == len(self) return len(self.play_counts.values()) == len(self)
@property @property
def requests(self): def requests(self):
@@ -225,7 +221,7 @@ class Cassette(object):
@property @property
def write_protected(self): def write_protected(self):
return self.rewound and self.record_mode == "once" or self.record_mode == "none" return self.rewound and self.record_mode == RecordMode.ONCE or self.record_mode == RecordMode.NONE
def append(self, request, response): def append(self, request, response):
"""Add a request, response pair to this cassette""" """Add a request, response pair to this cassette"""
@@ -257,7 +253,7 @@ class Cassette(object):
def can_play_response_for(self, request): def can_play_response_for(self, request):
request = self._before_record_request(request) request = self._before_record_request(request)
return request and request in self and self.record_mode != "all" and self.rewound return request and request in self and self.record_mode != RecordMode.ALL and self.rewound
def play_response(self, request): def play_response(self, request):
""" """
@@ -265,7 +261,7 @@ class Cassette(object):
hasn't been played back before, and mark it as played hasn't been played back before, and mark it as played
""" """
for index, response in self._responses(request): for index, response in self._responses(request):
if self.play_counts[index] == 0: if self.play_counts[index] == 0 or self.allow_playback_repeats:
self.play_counts[index] += 1 self.play_counts[index] += 1
return response return response
# The cassette doesn't contain the request asked for. # The cassette doesn't contain the request asked for.
@@ -355,6 +351,6 @@ class Cassette(object):
def __contains__(self, request): def __contains__(self, request):
"""Return whether or not a request has been stored""" """Return whether or not a request has been stored"""
for index, response in self._responses(request): for index, response in self._responses(request):
if self.play_counts[index] == 0: if self.play_counts[index] == 0 or self.allow_playback_repeats:
return True return True
return False return False

View File

@@ -1,14 +0,0 @@
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
__all__ = ["mock", "contextlib"]

View File

@@ -1,9 +1,6 @@
import copy import copy
try: from collections import abc as collections_abc
from collections import abc as collections_abc # only works on python 3.3+
except ImportError:
import collections as collections_abc
import functools import functools
import inspect import inspect
import os import os
@@ -15,11 +12,12 @@ from .cassette import Cassette
from .serializers import yamlserializer, jsonserializer from .serializers import yamlserializer, jsonserializer
from .persisters.filesystem import FilesystemPersister from .persisters.filesystem import FilesystemPersister
from .util import compose, auto_decorate from .util import compose, auto_decorate
from .record_mode import RecordMode
from . import matchers from . import matchers
from . import filters from . import filters
class VCR(object): class VCR:
@staticmethod @staticmethod
def is_test_method(method_name, function): def is_test_method(method_name, function):
return method_name.startswith("test") and isinstance(function, types.FunctionType) return method_name.startswith("test") and isinstance(function, types.FunctionType)
@@ -40,7 +38,7 @@ class VCR(object):
custom_patches=(), custom_patches=(),
filter_query_parameters=(), filter_query_parameters=(),
ignore_hosts=(), ignore_hosts=(),
record_mode="once", record_mode=RecordMode.ONCE,
ignore_localhost=False, ignore_localhost=False,
filter_headers=(), filter_headers=(),
before_record_response=None, before_record_response=None,
@@ -102,7 +100,7 @@ class VCR(object):
return matchers return matchers
def use_cassette(self, path=None, **kwargs): def use_cassette(self, path=None, **kwargs):
if path is not None and not isinstance(path, six.string_types): if path is not None and not isinstance(path, str):
function = path function = path
# Assume this is an attempt to decorate a function # Assume this is an attempt to decorate a function
return self._use_cassette(**kwargs)(function) return self._use_cassette(**kwargs)(function)
@@ -151,6 +149,7 @@ class VCR(object):
"inject": kwargs.get("inject_cassette", self.inject_cassette), "inject": kwargs.get("inject_cassette", self.inject_cassette),
"path_transformer": path_transformer, "path_transformer": path_transformer,
"func_path_generator": func_path_generator, "func_path_generator": func_path_generator,
"allow_playback_repeats": kwargs.get("allow_playback_repeats", False),
} }
path = kwargs.get("path") path = kwargs.get("path")
if path: if path:
@@ -251,4 +250,5 @@ class VCR(object):
def test_case(self, predicate=None): def test_case(self, predicate=None):
predicate = predicate or self.is_test_method predicate = predicate or self.is_test_method
# TODO: Remove this reference to `six` in favor of the Python3 equivalent
return six.with_metaclass(auto_decorate(self.use_cassette, predicate)) return six.with_metaclass(auto_decorate(self.use_cassette, predicate))

View File

@@ -3,7 +3,7 @@ class CannotOverwriteExistingCassetteException(Exception):
self.cassette = kwargs["cassette"] self.cassette = kwargs["cassette"]
self.failed_request = kwargs["failed_request"] self.failed_request = kwargs["failed_request"]
message = self._get_message(kwargs["cassette"], kwargs["failed_request"]) message = self._get_message(kwargs["cassette"], kwargs["failed_request"])
super(CannotOverwriteExistingCassetteException, self).__init__(message) super().__init__(message)
@staticmethod @staticmethod
def _get_message(cassette, failed_request): def _get_message(cassette, failed_request):

View File

@@ -1,5 +1,5 @@
from six import BytesIO, text_type from io import BytesIO
from six.moves.urllib.parse import urlparse, urlencode, urlunparse from urllib.parse import urlparse, urlencode, urlunparse
import copy import copy
import json import json
import zlib import zlib
@@ -8,13 +8,11 @@ from .util import CaseInsensitiveDict
def replace_headers(request, replacements): def replace_headers(request, replacements):
""" """Replace headers in request according to replacements.
Replace headers in request according to replacements. The replacements The replacements should be a list of (key, value) pairs where the value can be any of:
should be a list of (key, value) pairs where the value can be any of: 1. A simple replacement string value.
1. A simple replacement string value. 2. None to remove the given header.
2. None to remove the given header. 3. A callable which accepts (key, value, request) and returns a string value or None.
3. A callable which accepts (key, value, request) and returns a string
value or None.
""" """
new_headers = request.headers.copy() new_headers = request.headers.copy()
for k, rv in replacements: for k, rv in replacements:
@@ -37,10 +35,9 @@ def remove_headers(request, headers_to_remove):
def replace_query_parameters(request, replacements): def replace_query_parameters(request, replacements):
""" """Replace query parameters in request according to replacements.
Replace query parameters in request according to replacements. The
replacements should be a list of (key, value) pairs where the value can be The replacements should be a list of (key, value) pairs where the value can be any of:
any of:
1. A simple replacement string value. 1. A simple replacement string value.
2. None to remove the given header. 2. None to remove the given header.
3. A callable which accepts (key, value, request) and returns a string 3. A callable which accepts (key, value, request) and returns a string
@@ -73,10 +70,9 @@ def remove_query_parameters(request, query_parameters_to_remove):
def replace_post_data_parameters(request, replacements): def replace_post_data_parameters(request, replacements):
""" """Replace post data in request--either form data or json--according to replacements.
Replace post data in request--either form data or json--according to
replacements. The replacements should be a list of (key, value) pairs where The replacements should be a list of (key, value) pairs where the value can be any of:
the value can be any of:
1. A simple replacement string value. 1. A simple replacement string value.
2. None to remove the given header. 2. None to remove the given header.
3. A callable which accepts (key, value, request) and returns a string 3. A callable which accepts (key, value, request) and returns a string
@@ -99,7 +95,7 @@ def replace_post_data_parameters(request, replacements):
json_data[k] = rv json_data[k] = rv
request.body = json.dumps(json_data).encode("utf-8") request.body = json.dumps(json_data).encode("utf-8")
else: else:
if isinstance(request.body, text_type): if isinstance(request.body, str):
request.body = request.body.encode("utf-8") request.body = request.body.encode("utf-8")
splits = [p.partition(b"=") for p in request.body.split(b"&")] splits = [p.partition(b"=") for p in request.body.split(b"&")]
new_splits = [] new_splits = []

View File

@@ -1,5 +1,6 @@
import json import json
from six.moves import urllib, xmlrpc_client import urllib
import xmlrpc.client
from .util import read_body from .util import read_body
import logging import logging
@@ -77,7 +78,7 @@ _checker_transformer_pairs = (
lambda body: urllib.parse.parse_qs(body.decode("ascii")), lambda body: urllib.parse.parse_qs(body.decode("ascii")),
), ),
(_header_checker("application/json"), _transform_json), (_header_checker("application/json"), _transform_json),
(lambda request: _xml_header_checker(request) and _xmlrpc_header_checker(request), xmlrpc_client.loads), (lambda request: _xml_header_checker(request) and _xmlrpc_header_checker(request), xmlrpc.client.loads),
) )

View File

@@ -1,10 +1,11 @@
"""Utilities for patching in cassettes""" """Utilities for patching in cassettes"""
import contextlib
import functools import functools
import itertools import itertools
from unittest import mock
from .compat import contextlib, mock
from .stubs import VCRHTTPConnection, VCRHTTPSConnection from .stubs import VCRHTTPConnection, VCRHTTPSConnection
from six.moves import http_client as httplib import http.client as httplib
import logging import logging
@@ -93,7 +94,16 @@ else:
_AiohttpClientSessionRequest = aiohttp.client.ClientSession._request _AiohttpClientSessionRequest = aiohttp.client.ClientSession._request
class CassettePatcherBuilder(object): try:
import httpx
except ImportError: # pragma: no cover
pass
else:
_HttpxSyncClient_send = httpx.Client.send
_HttpxAsyncClient_send = httpx.AsyncClient.send
class CassettePatcherBuilder:
def _build_patchers_from_mock_triples_decorator(function): def _build_patchers_from_mock_triples_decorator(function):
@functools.wraps(function) @functools.wraps(function)
def wrapped(self, *args, **kwargs): def wrapped(self, *args, **kwargs):
@@ -115,6 +125,7 @@ class CassettePatcherBuilder(object):
self._boto(), self._boto(),
self._tornado(), self._tornado(),
self._aiohttp(), self._aiohttp(),
self._httpx(),
self._build_patchers_from_mock_triples(self._cassette.custom_patches), self._build_patchers_from_mock_triples(self._cassette.custom_patches),
) )
@@ -312,6 +323,21 @@ class CassettePatcherBuilder(object):
new_request = vcr_request(self._cassette, _AiohttpClientSessionRequest) new_request = vcr_request(self._cassette, _AiohttpClientSessionRequest)
yield client.ClientSession, "_request", new_request yield client.ClientSession, "_request", new_request
@_build_patchers_from_mock_triples_decorator
def _httpx(self):
try:
import httpx
except ImportError: # pragma: no cover
return
else:
from .stubs.httpx_stubs import async_vcr_send, sync_vcr_send
new_async_client_send = async_vcr_send(self._cassette, _HttpxAsyncClient_send)
yield httpx.AsyncClient, "send", new_async_client_send
new_sync_client_send = sync_vcr_send(self._cassette, _HttpxSyncClient_send)
yield httpx.Client, "send", new_sync_client_send
def _urllib3_patchers(self, cpool, stubs): def _urllib3_patchers(self, cpool, stubs):
http_connection_remover = ConnectionRemover( http_connection_remover = ConnectionRemover(
self._get_cassette_subclass(stubs.VCRRequestsHTTPConnection) self._get_cassette_subclass(stubs.VCRRequestsHTTPConnection)
@@ -358,7 +384,7 @@ class CassettePatcherBuilder(object):
) )
class ConnectionRemover(object): class ConnectionRemover:
def __init__(self, connection_class): def __init__(self, connection_class):
self._connection_class = connection_class self._connection_class = connection_class
self._connection_pool_to_connections = {} self._connection_pool_to_connections = {}

View File

@@ -4,13 +4,13 @@ import os
from ..serialize import serialize, deserialize from ..serialize import serialize, deserialize
class FilesystemPersister(object): class FilesystemPersister:
@classmethod @classmethod
def load_cassette(cls, cassette_path, serializer): def load_cassette(cls, cassette_path, serializer):
try: try:
with open(cassette_path) as f: with open(cassette_path) as f:
cassette_content = f.read() cassette_content = f.read()
except IOError: except OSError:
raise ValueError("Cassette not found.") raise ValueError("Cassette not found.")
cassette = deserialize(cassette_content, serializer) cassette = deserialize(cassette_content, serializer)
return cassette return cassette

23
vcr/record_mode.py Normal file
View File

@@ -0,0 +1,23 @@
from enum import Enum
class RecordMode(str, Enum):
"""
Configues when VCR will record to the cassette.
Can be declared by either using the enumerated value (`vcr.mode.ONCE`)
or by simply using the defined string (`once`).
`ALL`: Every request is recorded.
`ANY`: ?
`NEW_EPISODES`: Any request not found in the cassette is recorded.
`NONE`: No requests are recorded.
`ONCE`: First set of requests is recorded, all others are replayed.
Attempting to add a new episode fails.
"""
ALL = "all"
ANY = "any"
NEW_EPISODES = "new_episodes"
NONE = "none"
ONCE = "once"

View File

@@ -1,13 +1,13 @@
import warnings import warnings
from six import BytesIO, text_type from io import BytesIO
from six.moves.urllib.parse import urlparse, parse_qsl from urllib.parse import urlparse, parse_qsl
from .util import CaseInsensitiveDict from .util import CaseInsensitiveDict
import logging import logging
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
class Request(object): class Request:
""" """
VCR's representation of a request. VCR's representation of a request.
""" """
@@ -39,7 +39,7 @@ class Request(object):
@body.setter @body.setter
def body(self, value): def body(self, value):
if isinstance(value, text_type): if isinstance(value, str):
value = value.encode("utf-8") value = value.encode("utf-8")
self._body = value self._body = value
@@ -136,4 +136,4 @@ class HeadersDict(CaseInsensitiveDict):
if old: if old:
key = old[0] key = old[0]
super(HeadersDict, self).__setitem__(key, value) super().__setitem__(key, value)

View File

@@ -1,6 +1,3 @@
import six
def convert_to_bytes(resp): def convert_to_bytes(resp):
resp = convert_body_to_bytes(resp) resp = convert_body_to_bytes(resp)
return resp return resp
@@ -24,7 +21,7 @@ def convert_body_to_bytes(resp):
http://pyyaml.org/wiki/PyYAMLDocumentation#Python3support http://pyyaml.org/wiki/PyYAMLDocumentation#Python3support
""" """
try: try:
if resp["body"]["string"] is not None and not isinstance(resp["body"]["string"], six.binary_type): if resp["body"]["string"] is not None and not isinstance(resp["body"]["string"], bytes):
resp["body"]["string"] = resp["body"]["string"].encode("utf-8") resp["body"]["string"] = resp["body"]["string"].encode("utf-8")
except (KeyError, TypeError, UnicodeEncodeError): except (KeyError, TypeError, UnicodeEncodeError):
# The thing we were converting either wasn't a dictionary or didn't # The thing we were converting either wasn't a dictionary or didn't
@@ -44,7 +41,7 @@ def _convert_string_to_unicode(string):
result = string result = string
try: try:
if string is not None and not isinstance(string, six.text_type): if string is not None and not isinstance(string, str):
result = string.decode("utf-8") result = string.decode("utf-8")
except (TypeError, UnicodeDecodeError, AttributeError): except (TypeError, UnicodeDecodeError, AttributeError):
# Sometimes the string actually is binary or StringIO object, # Sometimes the string actually is binary or StringIO object,

View File

@@ -16,7 +16,7 @@ def serialize(cassette_dict):
) )
try: try:
return json.dumps(cassette_dict, indent=4) return json.dumps(cassette_dict, indent=4) + "\n"
except UnicodeDecodeError as original: # py2 except UnicodeDecodeError as original: # py2
raise UnicodeDecodeError( raise UnicodeDecodeError(
original.encoding, original.encoding,

View File

@@ -1,9 +1,12 @@
"""Stubs for patching HTTP and HTTPS requests""" """Stubs for patching HTTP and HTTPS requests"""
import logging import logging
import six from http.client import (
from six.moves.http_client import HTTPConnection, HTTPSConnection, HTTPResponse HTTPConnection,
from six import BytesIO HTTPSConnection,
HTTPResponse,
)
from io import BytesIO
from vcr.request import Request from vcr.request import Request
from vcr.errors import CannotOverwriteExistingCassetteException from vcr.errors import CannotOverwriteExistingCassetteException
from . import compat from . import compat
@@ -11,7 +14,7 @@ from . import compat
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
class VCRFakeSocket(object): class VCRFakeSocket:
""" """
A socket that doesn't do anything! A socket that doesn't do anything!
Used when playing back cassettes, when there Used when playing back cassettes, when there
@@ -143,7 +146,7 @@ class VCRHTTPResponse(HTTPResponse):
return self._content.readable() return self._content.readable()
class VCRConnection(object): class VCRConnection:
# A reference to the cassette that's currently being patched in # A reference to the cassette that's currently being patched in
cassette = None cassette = None
@@ -296,8 +299,7 @@ class VCRConnection(object):
self.real_connection.sock = value self.real_connection.sock = value
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
if six.PY3: kwargs.pop("strict", None) # apparently this is gone in py3
kwargs.pop("strict", None) # apparently this is gone in py3
# need to temporarily reset here because the real connection # need to temporarily reset here because the real connection
# inherits from the thing that we are mocking out. Take out # inherits from the thing that we are mocking out. Take out
@@ -328,7 +330,7 @@ class VCRConnection(object):
# we're setting the real_connection itself for the first time # we're setting the real_connection itself for the first time
pass pass
super(VCRConnection, self).__setattr__(name, value) super().__setattr__(name, value)
def __getattr__(self, name): def __getattr__(self, name):
""" """
@@ -340,7 +342,7 @@ class VCRConnection(object):
# we're setting the real_connection itself for the first time # we're setting the real_connection itself for the first time
return getattr(self.real_connection, name) return getattr(self.real_connection, name)
return super(VCRConnection, self).__getattr__(name) return super().__getattr__(name)
for k, v in HTTPConnection.__dict__.items(): for k, v in HTTPConnection.__dict__.items():

274
vcr/stubs/aiohttp_stubs.py Normal file
View File

@@ -0,0 +1,274 @@
"""Stubs for aiohttp HTTP clients"""
import asyncio
import functools
import logging
import json
from aiohttp import ClientConnectionError, ClientResponse, RequestInfo, streams
from aiohttp import hdrs, CookieJar
from http.cookies import CookieError, Morsel, SimpleCookie
from aiohttp.helpers import strip_auth_from_url
from multidict import CIMultiDict, CIMultiDictProxy
from yarl import URL
from vcr.errors import CannotOverwriteExistingCassetteException
from vcr.request import Request
log = logging.getLogger(__name__)
class MockStream(asyncio.StreamReader, streams.AsyncStreamReaderMixin):
pass
class MockClientResponse(ClientResponse):
def __init__(self, method, url, request_info=None):
super().__init__(
method=method,
url=url,
writer=None,
continue100=None,
timer=None,
request_info=request_info,
traces=None,
loop=asyncio.get_event_loop(),
session=None,
)
async def json(self, *, encoding="utf-8", loads=json.loads, **kwargs): # NOQA: E999
stripped = self._body.strip()
if not stripped:
return None
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
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):
request_info = RequestInfo(
url=URL(vcr_request.url),
method=vcr_request.method,
headers=_deserialize_headers(vcr_request.headers),
real_url=URL(vcr_request.url),
)
response = MockClientResponse(vcr_request.method, URL(vcr_response.get("url")), request_info=request_info)
response.status = vcr_response["status"]["code"]
response._body = vcr_response["body"].get("string", b"")
response.reason = vcr_response["status"]["message"]
response._headers = _deserialize_headers(vcr_response["headers"])
response._history = tuple(history)
# cookies
for hdr in response.headers.getall(hdrs.SET_COOKIE, ()):
try:
cookies = SimpleCookie(hdr)
for cookie_name, cookie in cookies.items():
expires = cookie.get("expires", "").strip()
if expires:
log.debug('Ignoring expiration date: %s="%s"', cookie_name, expires)
cookie["expires"] = ""
response.cookies.load(cookie.output(header="").strip())
except CookieError as exc:
log.warning("Can not load response cookies: %s", exc)
response.close()
return response
def _serialize_headers(headers):
"""Serialize CIMultiDictProxy to a pickle-able dict because proxy
objects forbid pickling:
https://github.com/aio-libs/multidict/issues/340
"""
# Mark strings as keys so 'istr' types don't show up in
# the cassettes as comments.
serialized_headers = {}
for k, v in headers.items():
serialized_headers.setdefault(str(k), []).append(v)
return serialized_headers
def _deserialize_headers(headers):
deserialized_headers = CIMultiDict()
for k, vs in headers.items():
if isinstance(vs, list):
for v in vs:
deserialized_headers.add(k, v)
else:
deserialized_headers.add(k, vs)
return CIMultiDictProxy(deserialized_headers)
def play_responses(cassette, vcr_request):
history = []
vcr_response = cassette.play_response(vcr_request)
response = build_response(vcr_request, vcr_response, history)
# If we're following redirects, continue playing until we reach
# our final destination.
while 300 <= response.status <= 399:
if "location" not in response.headers:
break
next_url = URL(response.url).join(URL(response.headers["location"]))
# Make a stub VCR request that we can then use to look up the recorded
# VCR request saved to the cassette. This feels a little hacky and
# may have edge cases based on the headers we're providing (e.g. if
# there's a matcher that is used to filter by headers).
vcr_request = Request("GET", str(next_url), None, _serialize_headers(response.request_info.headers))
vcr_requests = cassette.find_requests_with_most_matches(vcr_request)
for vcr_request, *_ in vcr_requests:
if cassette.can_play_response_for(vcr_request):
break
# Tack on the response we saw from the redirect into the history
# list that is added on to the final response.
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):
"""Record a VCR request-response chain to the cassette."""
try:
body = {"string": (await response.read())}
# aiohttp raises a ClientConnectionError on reads when
# there is no body. We can use this to know to not write one.
except ClientConnectionError:
body = {}
vcr_response = {
"status": {"code": response.status, "message": response.reason},
"headers": _serialize_headers(response.headers),
"body": body, # NOQA: E999
"url": str(response.url),
}
cassette.append(vcr_request, vcr_response)
async def record_responses(cassette, vcr_request, response):
"""Because aiohttp follows redirects by default, we must support
them by default. This method is used to write individual
request-response chains that were implicitly followed to get
to the final destination.
"""
for past_response in response.history:
aiohttp_request = past_response.request_info
# No data because it's following a redirect.
past_request = Request(
aiohttp_request.method,
str(aiohttp_request.url),
None,
_serialize_headers(aiohttp_request.headers),
)
await record_response(cassette, past_request, past_response)
# If we're following redirects, then the last request-response
# we record is the one attached to the `response`.
if response.history:
aiohttp_request = response.request_info
vcr_request = Request(
aiohttp_request.method,
str(aiohttp_request.url),
None,
_serialize_headers(aiohttp_request.headers),
)
await record_response(cassette, vcr_request, response)
def _build_cookie_header(session, cookies, cookie_header, url):
url, _ = strip_auth_from_url(url)
all_cookies = session._cookie_jar.filter_cookies(url)
if cookies is not None:
tmp_cookie_jar = CookieJar()
tmp_cookie_jar.update_cookies(cookies)
req_cookies = tmp_cookie_jar.filter_cookies(url)
if req_cookies:
all_cookies.load(req_cookies)
if not all_cookies and not cookie_header:
return None
c = SimpleCookie()
if cookie_header:
c.load(cookie_header)
for name, value in all_cookies.items():
if isinstance(value, Morsel):
mrsl_val = value.get(value.key, Morsel())
mrsl_val.set(value.key, value.value, value.coded_value)
c[name] = mrsl_val
else:
c[name] = value
return c.output(header="", sep=";").strip()
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", kwargs.get("json"))
params = kwargs.get("params")
cookies = kwargs.get("cookies")
if auth is not None:
headers["AUTHORIZATION"] = auth.encode()
request_url = URL(url)
if params:
for k, v in params.items():
params[k] = str(v)
request_url = URL(url).with_query(params)
c_header = headers.pop(hdrs.COOKIE, None)
cookie_header = _build_cookie_header(self, cookies, c_header, request_url)
if cookie_header:
headers[hdrs.COOKIE] = cookie_header
vcr_request = Request(method, str(request_url), data, _serialize_headers(headers))
if cassette.can_play_response_for(vcr_request):
log.info("Playing response for {} from cassette".format(vcr_request))
response = play_responses(cassette, vcr_request)
for redirect in response.history:
self._cookie_jar.update_cookies(redirect.cookies, redirect.url)
self._cookie_jar.update_cookies(response.cookies, response.url)
return response
if cassette.write_protected and cassette.filter_request(vcr_request):
raise CannotOverwriteExistingCassetteException(cassette=cassette, failed_request=vcr_request)
log.info("%s not in cassette, sending to real server", vcr_request)
response = await real_request(self, method, url, **kwargs) # NOQA: E999
await record_responses(cassette, vcr_request, response)
return response
return new_request

View File

@@ -1,146 +0,0 @@
"""Stubs for aiohttp HTTP clients"""
from __future__ import absolute_import
import asyncio
import functools
import logging
import json
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):
super().__init__(
method=method,
url=url,
writer=None,
continue100=None,
timer=None,
request_info=None,
traces=None,
loop=asyncio.get_event_loop(),
session=None,
)
async def json(self, *, encoding="utf-8", loads=json.loads, **kwargs): # NOQA: E999
stripped = self._body.strip()
if not stripped:
return None
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
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", 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():
params[k] = str(v)
request_url = URL(url).with_query(params)
vcr_request = Request(method, str(request_url), data, headers)
if cassette.can_play_response_for(vcr_request):
return play_responses(cassette, vcr_request)
if cassette.write_protected and cassette.filter_request(vcr_request):
response = MockClientResponse(method, URL(url))
response.status = 599
msg = (
"No match for the request {!r} was found. Can't overwrite "
"existing cassette {!r} in your current record mode {!r}."
)
msg = msg.format(vcr_request, cassette._path, cassette.record_mode)
response._body = msg.encode()
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
await record_responses(cassette, vcr_request, response)
return response
return new_request

View File

@@ -1,6 +1,4 @@
"""Stubs for boto3""" """Stubs for boto3"""
import six
try: try:
# boto using awsrequest # boto using awsrequest
from botocore.awsrequest import AWSHTTPConnection as HTTPConnection from botocore.awsrequest import AWSHTTPConnection as HTTPConnection
@@ -26,8 +24,7 @@ class VCRRequestsHTTPSConnection(VCRHTTPSConnection, VerifiedHTTPSConnection):
_baseclass = VerifiedHTTPSConnection _baseclass = VerifiedHTTPSConnection
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
if six.PY3: kwargs.pop("strict", None)
kwargs.pop("strict", None) # apparently this is gone in py3
# need to temporarily reset here because the real connection # need to temporarily reset here because the real connection
# inherits from the thing that we are mocking out. Take out # inherits from the thing that we are mocking out. Take out

View File

@@ -1,11 +1,5 @@
import six from io import BytesIO
from six import BytesIO import http.client
from six.moves.http_client import HTTPMessage
try:
import http.client
except ImportError:
pass
""" """
@@ -15,10 +9,7 @@ layer that tries to cope with this move.
def get_header(message, name): def get_header(message, name):
if six.PY3: return message.getallmatchingheaders(name)
return message.getallmatchingheaders(name)
else:
return message.getheader(name)
def get_header_items(message): def get_header_items(message):
@@ -29,16 +20,8 @@ def get_header_items(message):
def get_headers(message): def get_headers(message):
for key in set(message.keys()): for key in set(message.keys()):
if six.PY3: yield key, message.get_all(key)
yield key, message.get_all(key)
else:
yield key, message.getheaders(key)
def get_httpmessage(headers): def get_httpmessage(headers):
if six.PY3: return http.client.parse_headers(BytesIO(headers))
return http.client.parse_headers(BytesIO(headers))
msg = HTTPMessage(BytesIO(headers))
msg.fp.seek(0)
msg.readheaders()
return msg

158
vcr/stubs/httpx_stubs.py Normal file
View File

@@ -0,0 +1,158 @@
import functools
import logging
from unittest.mock import patch, MagicMock
import httpx
from vcr.request import Request as VcrRequest
from vcr.errors import CannotOverwriteExistingCassetteException
_logger = logging.getLogger(__name__)
def _transform_headers(httpx_reponse):
"""
Some headers can appear multiple times, like "Set-Cookie".
Therefore transform to every header key to list of values.
"""
out = {}
for key, var in httpx_reponse.headers.raw:
decoded_key = key.decode("utf-8")
out.setdefault(decoded_key, [])
out[decoded_key].append(var.decode("utf-8"))
return out
def _to_serialized_response(httpx_reponse):
return {
"status_code": httpx_reponse.status_code,
"http_version": httpx_reponse.http_version,
"headers": _transform_headers(httpx_reponse),
"content": httpx_reponse.content.decode("utf-8", "ignore"),
}
def _from_serialized_headers(headers):
"""
httpx accepts headers as list of tuples of header key and value.
"""
header_list = []
for key, values in headers.items():
for v in values:
header_list.append((key, v))
return header_list
@patch("httpx.Response.close", MagicMock())
@patch("httpx.Response.read", MagicMock())
def _from_serialized_response(request, serialized_response, history=None):
content = serialized_response.get("content").encode()
response = httpx.Response(
status_code=serialized_response.get("status_code"),
request=request,
headers=_from_serialized_headers(serialized_response.get("headers")),
content=content,
history=history or [],
)
response._content = content
return response
def _make_vcr_request(httpx_request, **kwargs):
body = httpx_request.read().decode("utf-8")
uri = str(httpx_request.url)
headers = dict(httpx_request.headers)
return VcrRequest(httpx_request.method, uri, body, headers)
def _shared_vcr_send(cassette, real_send, *args, **kwargs):
real_request = args[1]
vcr_request = _make_vcr_request(real_request, **kwargs)
if cassette.can_play_response_for(vcr_request):
return vcr_request, _play_responses(cassette, real_request, vcr_request, args[0], kwargs)
if cassette.write_protected and cassette.filter_request(vcr_request):
raise CannotOverwriteExistingCassetteException(cassette=cassette, failed_request=vcr_request)
_logger.info("%s not in cassette, sending to real server", vcr_request)
return vcr_request, None
def _record_responses(cassette, vcr_request, real_response):
for past_real_response in real_response.history:
past_vcr_request = _make_vcr_request(past_real_response.request)
cassette.append(past_vcr_request, _to_serialized_response(past_real_response))
if real_response.history:
# If there was a redirection keep we want the request which will hold the
# final redirect value
vcr_request = _make_vcr_request(real_response.request)
cassette.append(vcr_request, _to_serialized_response(real_response))
return real_response
def _play_responses(cassette, request, vcr_request, client, kwargs):
history = []
allow_redirects = kwargs.get("allow_redirects", True)
vcr_response = cassette.play_response(vcr_request)
response = _from_serialized_response(request, vcr_response)
while allow_redirects and 300 <= response.status_code <= 399:
next_url = response.headers.get("location")
if not next_url:
break
vcr_request = VcrRequest("GET", next_url, None, dict(response.headers))
vcr_request = cassette.find_requests_with_most_matches(vcr_request)[0][0]
history.append(response)
# add cookies from response to session cookie store
client.cookies.extract_cookies(response)
vcr_response = cassette.play_response(vcr_request)
response = _from_serialized_response(vcr_request, vcr_response, history)
return response
async def _async_vcr_send(cassette, real_send, *args, **kwargs):
vcr_request, response = _shared_vcr_send(cassette, real_send, *args, **kwargs)
if response:
# add cookies from response to session cookie store
args[0].cookies.extract_cookies(response)
return response
real_response = await real_send(*args, **kwargs)
return _record_responses(cassette, vcr_request, real_response)
def async_vcr_send(cassette, real_send):
@functools.wraps(real_send)
def _inner_send(*args, **kwargs):
return _async_vcr_send(cassette, real_send, *args, **kwargs)
return _inner_send
def _sync_vcr_send(cassette, real_send, *args, **kwargs):
vcr_request, response = _shared_vcr_send(cassette, real_send, *args, **kwargs)
if response:
# add cookies from response to session cookie store
args[0].cookies.extract_cookies(response)
return response
real_response = real_send(*args, **kwargs)
return _record_responses(cassette, vcr_request, real_response)
def sync_vcr_send(cassette, real_send):
@functools.wraps(real_send)
def _inner_send(*args, **kwargs):
return _sync_vcr_send(cassette, real_send, *args, **kwargs)
return _inner_send

View File

@@ -1,8 +1,6 @@
"""Stubs for tornado HTTP clients""" """Stubs for tornado HTTP clients"""
from __future__ import absolute_import
import functools import functools
from six import BytesIO from io import BytesIO
from tornado import httputil from tornado import httputil
from tornado.httpclient import HTTPResponse from tornado.httpclient import HTTPResponse

View File

@@ -107,12 +107,12 @@ def auto_decorate(decorator, predicate=lambda name, value: isinstance(value, typ
class DecorateAll(type): class DecorateAll(type):
def __setattr__(cls, attribute, value): def __setattr__(cls, attribute, value):
return super(DecorateAll, cls).__setattr__(attribute, maybe_decorate(attribute, value)) return super().__setattr__(attribute, maybe_decorate(attribute, value))
def __new__(cls, name, bases, attributes_dict): def __new__(cls, name, bases, attributes_dict):
new_attributes_dict = { new_attributes_dict = {
attribute: maybe_decorate(attribute, value) for attribute, value in attributes_dict.items() attribute: maybe_decorate(attribute, value) for attribute, value in attributes_dict.items()
} }
return super(DecorateAll, cls).__new__(cls, name, bases, new_attributes_dict) return super().__new__(cls, name, bases, new_attributes_dict)
return DecorateAll return DecorateAll