mirror of
https://github.com/kevin1024/vcrpy.git
synced 2025-12-08 16:53:23 +00:00
Compare commits
227 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d6bded1820 | ||
|
|
e7c00a4bf9 | ||
|
|
92dd4d00f7 | ||
|
|
cf3ffcad61 | ||
|
|
3ad462e766 | ||
|
|
cdab3fcb30 | ||
|
|
d3a5f4dd6c | ||
|
|
75c8607fd2 | ||
|
|
8c075c7fb3 | ||
|
|
a045a46bb4 | ||
|
|
1d979b078d | ||
|
|
f7d76bd40a | ||
|
|
7e11cfc9e4 | ||
|
|
c95b7264a2 | ||
|
|
8ab8e63e04 | ||
|
|
d2c1da9ab7 | ||
|
|
8336d66976 | ||
|
|
e69b10c2e0 | ||
|
|
a6b9a070a5 | ||
|
|
e35205c5c8 | ||
|
|
05f61ea56c | ||
|
|
943cabb14f | ||
|
|
4f70152e7c | ||
|
|
016a394f2c | ||
|
|
6b2fc182c3 | ||
|
|
a77173c002 | ||
|
|
34d5384318 | ||
|
|
ad1010d0f8 | ||
|
|
d99593bcd3 | ||
|
|
8c03c37df4 | ||
|
|
b827cbe2da | ||
|
|
92ca5a102c | ||
|
|
d2281ab646 | ||
|
|
f21c8f0224 | ||
|
|
8b97fd6551 | ||
|
|
29e42211d7 | ||
|
|
6e511b67fd | ||
|
|
9b6cb1ce23 | ||
|
|
6a12bd1511 | ||
|
|
3411bedc06 | ||
|
|
438a65426b | ||
|
|
8c6b1fdf38 | ||
|
|
15e9f1868c | ||
|
|
7eb235cd9c | ||
|
|
d2f2731481 | ||
|
|
b2a895cb89 | ||
|
|
ffb2f44236 | ||
|
|
d66392a3fb | ||
|
|
b9cab239a7 | ||
|
|
276a41d9b6 | ||
|
|
7007e944ae | ||
|
|
bd112a2385 | ||
|
|
42848285a0 | ||
|
|
e3aae34ef7 | ||
|
|
f4316d2dae | ||
|
|
d613a814d3 | ||
|
|
ce234e503f | ||
|
|
3527d25ce8 | ||
|
|
dedb7ec403 | ||
|
|
59263d6025 | ||
|
|
2842cabec6 | ||
|
|
ad650a7ee1 | ||
|
|
9232915885 | ||
|
|
cbb540029f | ||
|
|
bf30d9a5e5 | ||
|
|
f06f71ece4 | ||
|
|
1070d417b3 | ||
|
|
46726a9a61 | ||
|
|
87db8e69ff | ||
|
|
52701ebca4 | ||
|
|
69679dc3fc | ||
|
|
c13f33b1e0 | ||
|
|
5476dd010c | ||
|
|
0add77d5ae | ||
|
|
96a6e91def | ||
|
|
3b41f0ede3 | ||
|
|
0e06836908 | ||
|
|
69db5c936f | ||
|
|
7c402ae4b0 | ||
|
|
b5c0938d2e | ||
|
|
3ad93fff42 | ||
|
|
89f2005250 | ||
|
|
88c0039089 | ||
|
|
1b3a1235f2 | ||
|
|
fd1aaab3bf | ||
|
|
00da5ac5af | ||
|
|
ac20cd1dd3 | ||
|
|
64d6811eda | ||
|
|
51c99bb9df | ||
|
|
43484e7cff | ||
|
|
199f9f07f8 | ||
|
|
13af8cae43 | ||
|
|
436b62f587 | ||
|
|
5b40a67b3b | ||
|
|
c41bd2bb40 | ||
|
|
62cb151918 | ||
|
|
1a3bc67c7c | ||
|
|
aeff51bd79 | ||
|
|
e9f0ede9dd | ||
|
|
0235eab766 | ||
|
|
31c8dc0a1e | ||
|
|
24af48d468 | ||
|
|
44359bfe43 | ||
|
|
14cef83c15 | ||
|
|
77da67ef0a | ||
|
|
58329f812b | ||
|
|
06913ce21a | ||
|
|
4994c53590 | ||
|
|
1d90853f3b | ||
|
|
42d79b1102 | ||
|
|
cef85a4986 | ||
|
|
964615af25 | ||
|
|
3b6d79fc0b | ||
|
|
f48922ce09 | ||
|
|
2980bfccde | ||
|
|
7599f4d50a | ||
|
|
995020bf06 | ||
|
|
423ccaa40b | ||
|
|
526fdbb194 | ||
|
|
511d0ab855 | ||
|
|
60ac99c907 | ||
|
|
57dee93e11 | ||
|
|
0eece7f96e | ||
|
|
eb59d871b4 | ||
|
|
f3f66086a1 | ||
|
|
c3767c2fdb | ||
|
|
6fff3ab952 | ||
|
|
35378d046f | ||
|
|
9c46831a8e | ||
|
|
fe596447ec | ||
|
|
be1035fd5d | ||
|
|
eb96c590ff | ||
|
|
7add8c0bab | ||
|
|
b1bc5c3a02 | ||
|
|
a79356cf5f | ||
|
|
86806aa9bc | ||
|
|
7e73085331 | ||
|
|
3da66c8dee | ||
|
|
f5ea0304da | ||
|
|
25f715bc42 | ||
|
|
7d7164d7c7 | ||
|
|
fb065751dc | ||
|
|
874cf06407 | ||
|
|
b0e83986f0 | ||
|
|
8c0bb73658 | ||
|
|
43182d97de | ||
|
|
193210de49 | ||
|
|
e05ebca5e5 | ||
|
|
cd72278062 | ||
|
|
3c7b791783 | ||
|
|
7592efb8d9 | ||
|
|
5b2fc2712e | ||
|
|
c596a160b3 | ||
|
|
e68aa84649 | ||
|
|
678d56f608 | ||
|
|
d4927627c3 | ||
|
|
61b83aca7f | ||
|
|
0ac66f4413 | ||
|
|
000f7448a7 | ||
|
|
c79a06f639 | ||
|
|
08ef4a8bc4 | ||
|
|
4991d3d22b | ||
|
|
b2193828ab | ||
|
|
9d414c8841 | ||
|
|
a172207df0 | ||
|
|
dda16ef1e5 | ||
|
|
1d0fdbaa13 | ||
|
|
a351621d92 | ||
|
|
f387950486 | ||
|
|
8529c46f00 | ||
|
|
5afa8f703a | ||
|
|
641d9e5d49 | ||
|
|
0ef400195b | ||
|
|
133423ce94 | ||
|
|
7d2d29de12 | ||
|
|
023e41bb4c | ||
|
|
6ae93ac820 | ||
|
|
04b7f4fc65 | ||
|
|
936feb7748 | ||
|
|
79d26ebb43 | ||
|
|
b8024de1b8 | ||
|
|
2f94d06e9b | ||
|
|
042ee790e2 | ||
|
|
a249781b97 | ||
|
|
b64e93aff2 | ||
|
|
4897a8e692 | ||
|
|
15d79e5b78 | ||
|
|
1b9f80d741 | ||
|
|
20fb283e97 | ||
|
|
e868b64922 | ||
|
|
69cecdbda7 | ||
|
|
be53091ae5 | ||
|
|
ba91053485 | ||
|
|
837992767e | ||
|
|
cd0907ffaf | ||
|
|
77d838e0fc | ||
|
|
5362db2ebb | ||
|
|
f9ce14d29a | ||
|
|
5242e68cd1 | ||
|
|
9817a8bda5 | ||
|
|
6e1768b85b | ||
|
|
062126e50c | ||
|
|
438550959f | ||
|
|
69e4316545 | ||
|
|
2f53776ffb | ||
|
|
535efe1eb9 | ||
|
|
eb2e226bb8 | ||
|
|
8fe2ab6d06 | ||
|
|
6ac535f18d | ||
|
|
bceaab8b88 | ||
|
|
0c2bbe0d51 | ||
|
|
2e5fdd36d5 | ||
|
|
f8b9a41f13 | ||
|
|
6e040030b8 | ||
|
|
accffa8ea2 | ||
|
|
f07083e7cc | ||
|
|
4495dbd1ce | ||
|
|
357e3b03c5 | ||
|
|
566a70ab3a | ||
|
|
4a8e80ee3e | ||
|
|
41ca5750e7 | ||
|
|
d91a53bbff | ||
|
|
88092eb3cd | ||
|
|
4fcc5472bc | ||
|
|
8245bd4f84 | ||
|
|
531685d50b | ||
|
|
9ec19dd966 |
14
.editorconfig
Normal file
14
.editorconfig
Normal file
@@ -0,0 +1,14 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
|
||||
[Makefile]
|
||||
indent_style = tab
|
||||
|
||||
[*.{yml,yaml}]
|
||||
indent_size = 2
|
||||
11
.github/dependabot.yml
vendored
Normal file
11
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
version: 2
|
||||
|
||||
updates:
|
||||
- package-ecosystem: pip
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: weekly
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: weekly
|
||||
22
.github/workflows/codespell.yml
vendored
Normal file
22
.github/workflows/codespell.yml
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
---
|
||||
name: Codespell
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
pull_request:
|
||||
branches: [master]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
codespell:
|
||||
name: Check for spelling errors
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
- name: Codespell
|
||||
uses: codespell-project/actions-codespell@v2
|
||||
23
.github/workflows/docs.yml
vendored
Normal file
23
.github/workflows/docs.yml
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
name: Validate docs
|
||||
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- 'docs/**'
|
||||
|
||||
jobs:
|
||||
validate:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: "3.11"
|
||||
|
||||
- name: Install build dependencies
|
||||
run: pip install -r docs/requirements.txt
|
||||
- name: Rendering HTML documentation
|
||||
run: sphinx-build -b html docs/ html
|
||||
- name: Inspect html rendered
|
||||
run: cat html/index.html
|
||||
43
.github/workflows/main.yml
vendored
Normal file
43
.github/workflows/main.yml
vendored
Normal file
@@ -0,0 +1,43 @@
|
||||
name: Test
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
python-version: ["3.8", "3.9", "3.10", "3.11", "pypy-3.8"]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3.5.2
|
||||
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
- name: Install project dependencies
|
||||
run: |
|
||||
pip3 install --upgrade pip
|
||||
pip3 install codecov tox tox-gh-actions
|
||||
|
||||
- name: Run online tests with tox
|
||||
run: tox -- -m online
|
||||
|
||||
- name: Run offline tests with tox with no access to the Internet
|
||||
run: |
|
||||
# We're using unshare to take Internet access
|
||||
# away from tox so that we'll notice whenever some new test
|
||||
# is missing @pytest.mark.online decoration in the future
|
||||
unshare --map-root-user --net -- \
|
||||
sh -c 'ip link set lo up; tox -- -m "not online"'
|
||||
|
||||
- name: Run coverage
|
||||
run: codecov
|
||||
22
.readthedocs.yaml
Normal file
22
.readthedocs.yaml
Normal file
@@ -0,0 +1,22 @@
|
||||
# .readthedocs.yaml
|
||||
# Read the Docs configuration file
|
||||
# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
|
||||
|
||||
# Required
|
||||
version: 2
|
||||
|
||||
# Set the version of Python and other tools you might need
|
||||
build:
|
||||
os: ubuntu-22.04
|
||||
tools:
|
||||
python: "3.11"
|
||||
|
||||
# Build documentation in the docs/ directory with Sphinx
|
||||
sphinx:
|
||||
configuration: docs/conf.py
|
||||
|
||||
# We recommend specifying your dependencies to enable reproducible builds:
|
||||
# https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html
|
||||
python:
|
||||
install:
|
||||
- requirements: docs/requirements.txt
|
||||
44
.travis.yml
44
.travis.yml
@@ -1,44 +0,0 @@
|
||||
dist: xenial
|
||||
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:
|
||||
include:
|
||||
# Only run lint on a single 3.x
|
||||
- env: TOX_SUFFIX="lint"
|
||||
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:
|
||||
- "2.7"
|
||||
- "3.5"
|
||||
- "3.6"
|
||||
- "3.7"
|
||||
- "3.8-dev"
|
||||
- "pypy"
|
||||
- "pypy3"
|
||||
install:
|
||||
- pip install tox-travis codecov
|
||||
- if [[ $TOX_SUFFIX != 'lint' ]]; then python setup.py install ; fi
|
||||
script:
|
||||
- tox -e "${TOX_SUFFIX}"
|
||||
after_success:
|
||||
- codecov
|
||||
25
README.rst
25
README.rst
@@ -1,10 +1,16 @@
|
||||
|
||||
###########
|
||||
VCR.py 📼
|
||||
###########
|
||||
|
||||
|
||||
|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
|
||||
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
|
||||
API.
|
||||
|
||||
Usage with Pytest
|
||||
-----------------
|
||||
|
||||
There is a library to provide some pytest fixtures called pytest-recording https://github.com/kiwicom/pytest-recording
|
||||
|
||||
License
|
||||
=======
|
||||
-------
|
||||
|
||||
This library uses the MIT license. See `LICENSE.txt <LICENSE.txt>`__ for
|
||||
more details
|
||||
@@ -51,8 +62,8 @@ more details
|
||||
:target: https://pypi.python.org/pypi/vcrpy
|
||||
.. |Python versions| image:: https://img.shields.io/pypi/pyversions/vcrpy.svg
|
||||
:target: https://pypi.python.org/pypi/vcrpy
|
||||
.. |Build Status| image:: https://secure.travis-ci.org/kevin1024/vcrpy.svg?branch=master
|
||||
:target: http://travis-ci.org/kevin1024/vcrpy
|
||||
.. |Build Status| image:: https://github.com/kevin1024/vcrpy/actions/workflows/main.yml/badge.svg
|
||||
:target: https://github.com/kevin1024/vcrpy/actions
|
||||
.. |Gitter| image:: https://badges.gitter.im/Join%20Chat.svg
|
||||
:alt: Join the chat at https://gitter.im/kevin1024/vcrpy
|
||||
:target: https://gitter.im/kevin1024/vcrpy?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge
|
||||
|
||||
BIN
docs/_static/vcr.png
vendored
Normal file
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
27
docs/_static/vcr.svg
vendored
Normal 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 |
@@ -33,6 +33,8 @@ consider part of the API. The fields are as follows:
|
||||
been played back.
|
||||
- ``responses_of(request)``: Access the responses that match a given
|
||||
request
|
||||
- ``allow_playback_repeats``: A boolean indicating whether responses
|
||||
can be played back more than once.
|
||||
|
||||
The ``Request`` object has the following properties:
|
||||
|
||||
@@ -69,7 +71,7 @@ Finally, register your class with VCR to use your new serializer.
|
||||
|
||||
import vcr
|
||||
|
||||
class BogoSerializer(object):
|
||||
class BogoSerializer:
|
||||
"""
|
||||
Must implement serialize() and deserialize() methods
|
||||
"""
|
||||
@@ -130,11 +132,12 @@ Finally, register your method with VCR to use your new request matcher.
|
||||
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``
|
||||
methods. The ``load_cassette`` method must return a deserialized cassette or raise
|
||||
``ValueError`` if no cassette is found.
|
||||
either ``CassetteNotFoundError`` if no cassette is found, or ``CassetteDecodeError``
|
||||
if the cassette cannot be successfully deserialized.
|
||||
|
||||
Once the persister class is defined, register with VCR like so...
|
||||
|
||||
@@ -143,7 +146,7 @@ Once the persister class is defined, register with VCR like so...
|
||||
import vcr
|
||||
my_vcr = vcr.VCR()
|
||||
|
||||
class CustomerPersister(object):
|
||||
class CustomerPersister:
|
||||
# implement Persister methods...
|
||||
|
||||
my_vcr.register_persister(CustomPersister)
|
||||
@@ -186,7 +189,7 @@ of post data parameters to filter.
|
||||
|
||||
.. code:: python
|
||||
|
||||
with my_vcr.use_cassette('test.yml', filter_post_data_parameters=['client_secret']):
|
||||
with my_vcr.use_cassette('test.yml', filter_post_data_parameters=['api_key']):
|
||||
requests.post('http://api.com/postdata', data={'api_key': 'secretstring'})
|
||||
|
||||
Advanced use of filter_headers, filter_query_parameters and filter_post_data_parameters
|
||||
@@ -269,7 +272,7 @@ You can also do response filtering with the
|
||||
similar to the above ``before_record_request`` - you can
|
||||
mutate the response, or return ``None`` to avoid recording
|
||||
the request and response altogether. For example to hide
|
||||
sensitive data from the request body:
|
||||
sensitive data from the response body:
|
||||
|
||||
.. code:: python
|
||||
|
||||
@@ -384,5 +387,43 @@ 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:
|
||||
response = urllib2.urlopen('http://www.zombo.com/').read()
|
||||
assert cass.all_played
|
||||
a.rewind()
|
||||
cass.rewind()
|
||||
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
|
||||
|
||||
Discards Cassette on Errors
|
||||
---------------------------
|
||||
|
||||
By default VCR will save the cassette file even when there is any error inside
|
||||
the enclosing context/test.
|
||||
|
||||
If you want to save the cassette only when the test succeeds, set the Cassette
|
||||
``record_on_exception`` option to ``False``.
|
||||
|
||||
.. code:: python
|
||||
|
||||
try:
|
||||
my_vcr = VCR(record_on_exception=False)
|
||||
with my_vcr.use_cassette('fixtures/vcr_cassettes/synopsis.yaml') as cass:
|
||||
response = urllib2.urlopen('http://www.zombo.com/').read()
|
||||
raise RuntimeError("Oops, something happened")
|
||||
except RuntimeError:
|
||||
pass
|
||||
|
||||
# Since there was an exception, the cassette file hasn't been created.
|
||||
assert not os.path.exists('fixtures/vcr_cassettes/synopsis.yaml')
|
||||
|
||||
@@ -1,197 +1,289 @@
|
||||
Changelog
|
||||
---------
|
||||
- 2.1.x (UNRELEASED)
|
||||
- ....
|
||||
|
||||
For a full list of triaged issues, bugs and PRs and what release they are targeted 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.
|
||||
|
||||
- 5.1.0
|
||||
- Use ruff for linting (instead of current flake8/isort/pyflakes) - thanks @jairhenrique
|
||||
- Enable rule B (flake8-bugbear) on ruff - thanks @jairhenrique
|
||||
- Configure read the docs V2 - thanks @jairhenrique
|
||||
- Fix typo in docs - thanks @quasimik
|
||||
- Make json.loads of Python >=3.6 decode bytes by itself - thanks @hartwork
|
||||
- Fix body matcher for chunked requests (fixes #734) - thanks @hartwork
|
||||
- Fix query param filter for aiohttp (fixes #517) - thanks @hartwork and @salomvary
|
||||
- Remove unnecessary dependency on six. - thanks @charettes
|
||||
- build(deps): update sphinx requirement from <7 to <8 - thanks @jairhenrique
|
||||
- Add action to validate docs - thanks @jairhenrique
|
||||
- Add editorconfig file - thanks @jairhenrique
|
||||
- Drop iscoroutinefunction fallback function for unsupported python thanks @jairhenrique
|
||||
- 5.0.0
|
||||
- BREAKING CHANGE: Drop support for Python 3.7. 3.7 is EOL as of 6/27/23 Thanks @jairhenrique
|
||||
- BREAKING CHANGE: Custom Cassette persisters no longer catch ValueError. If you have implemented a custom persister (has anyone implemented a custom persister? Let us know!) then you will need to throw a CassetteNotFoundError when unable to find a cassette. See #681 for discussion and reason for this change. Thanks @amosjyng for the PR and the review from @hartwork
|
||||
- 4.4.0
|
||||
- HUGE thanks to @hartwork for all the work done on this release!
|
||||
- Bring vcr/unittest in to vcrpy as a full feature of vcr instead of a separate library. Big thanks to @hartwork for doing this and to @agriffis for originally creating the library
|
||||
- Make decompression robust towards already decompressed input (thanks @hartwork)
|
||||
- Bugfix: Add read1 method (fixes compatibility with biopython), thanks @mghantous
|
||||
- Bugfix: Prevent filters from corrupting request (thanks @abramclark)
|
||||
- Bugfix: Add support for `response.raw.stream()` to fix urllib v2 compat
|
||||
- Bugfix: Replace `assert` with `raise AssertionError`: fixes support for `PYTHONOPTIMIZE=1`
|
||||
- Add pytest.mark.online to run test suite offline, thanks @jspricke
|
||||
- use python3 and pip3 binaries to ease debian packaging (thanks @hartwork)
|
||||
- Add codespell (thanks @mghantous)
|
||||
- 4.3.1
|
||||
- Support urllib3 v1 and v2. NOTE: there is an issue running urllib3 v2 on
|
||||
Python older than 3.10, so this is currently blocked in the requirements.
|
||||
Hopefully we can resolve this situation in the future. Thanks to @shifqu,
|
||||
hartwork, jairhenrique, pquentin, and vEpiphyte for your work on this.
|
||||
- 4.3.0
|
||||
- Add support for Python 3.11 (Thanks @evgeni)
|
||||
- Drop support for botocore <1.11.0 and requests <2.16.2 (thanks @hartwork)
|
||||
- Bugfix: decode_compressed_response raises exception on empty responses. Thanks @CharString
|
||||
- Don't save requests from decorated tests if decorated test fails (thanks @dan-passaro)
|
||||
- Fix not calling all the exit stack when record_on_exception is False (thanks @Terseus)
|
||||
- Various CI, documentation, testing, and formatting improvements (Thanks @jairhenrique, @dan-passaro, @hartwork, and Terseus)
|
||||
- 4.2.1
|
||||
- Fix a bug where the first request in a redirect chain was not being recorded with aiohttp
|
||||
- Various typos and small fixes, thanks @jairhenrique, @timgates42
|
||||
- 4.2.0
|
||||
- Drop support for python < 3.7, thanks @jairhenrique, @IvanMalison, @AthulMuralidhar
|
||||
- Various aiohtt bigfixes (thanks @pauloromeira and boechat107)
|
||||
- Bugfix: filter_post_data_parameters not working with aiohttp. Thank you @vprakashplanview, @scop, @jairhenrique, and @cinemascop89
|
||||
- Bugfix: Some random misspellings (thanks @scop)
|
||||
- Migrate the CI suite to Github Actions from Travis (thanks @jairhenrique and @cclauss)
|
||||
- Various documentation and code misspelling fixes (thanks @scop and @Justintime50)
|
||||
- Bugfix: httpx support (select between allow_redirects/follow_redirects) (thanks @immerrr)
|
||||
- Bugfix: httpx support (select between allow_redirects/follow_redirects) (thanks @immerrr)
|
||||
- 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
|
||||
- Format code with black (@neozenith)
|
||||
- Use latest pypy3 in Travis (@hugovk)
|
||||
- Improve documentation about custom matchers (@gward)
|
||||
- Fix exception when body is empty (@keithprickett)
|
||||
- Add `pytest-recording` to the documentation as an alternative Pytest plugin (@Stranger6667)
|
||||
- Fix yarl and python3.5 version issue (@neozenith)
|
||||
- Fix header matcher for boto3 - fixes #474 (@simahawk)
|
||||
- 2.1.0 - Add a `rewind` method to reset a cassette (thanks @khamidou)
|
||||
New error message with more details on why the cassette failed to play a request (thanks @arthurHamon2, @neozenith)
|
||||
Handle connect tunnel URI (thanks @jeking3)
|
||||
Add code coverage to the project (thanks @neozenith)
|
||||
Drop support to python 3.4
|
||||
Add deprecation warning on python 2.7, next major release will drop python 2.7 support
|
||||
Fix build problems on requests tests (thanks to @dunossauro)
|
||||
Fix matching on 'body' failing when Unicode symbols are present in them (thanks @valgur)
|
||||
Fix bugs on aiohttp integration (thanks @graingert, @steinnes, @stj, @lamenezes, @lmazuel)
|
||||
Fix Biopython incompatibility (thanks @rishab121)
|
||||
Fix Boto3 integration (thanks @1oglop1, @arthurHamon2)
|
||||
- 2.0.1 - Fix bug when using vcrpy with python 3.4
|
||||
- 2.0.0 - Support python 3.7 (fix httplib2 and urllib2, thanks @felixonmars)
|
||||
[#356] Fixes `before_record_response` so the original response isn't changed (thanks @kgraves)
|
||||
Fix requests stub when using proxy (thanks @samuelfekete @daneoshiga)
|
||||
(only for aiohttp stub) Drop support to python 3.4 asyncio.coroutine (aiohttp doesn't support python it anymore)
|
||||
Fix aiohttp stub to work with aiohttp client (thanks @stj)
|
||||
Fix aiohttp stub to accept content type passed
|
||||
Improve docs (thanks @adamchainz)
|
||||
- 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.
|
||||
- 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)
|
||||
- 1.11.1 Fix compatibility with newest requests and urllib3 releases
|
||||
- 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.10.5 Added a fix to httplib2 (thanks @carlosds730), Fix an issue with
|
||||
aiohttp (thanks @madninja), Add missing requirement yarl (thanks @lamenezes),
|
||||
Remove duplicate mock triple (thanks @FooBarQuaxx)
|
||||
- 1.10.4 Fix an issue with asyncio aiohttp (thanks @madninja)
|
||||
- 1.10.3 Fix some issues with asyncio and params (thanks @anovikov1984 and
|
||||
@lamenezes), Fix some issues with cassette serialize / deserialize and empty
|
||||
response bodies (thanks @gRoussac and @dz0ny)
|
||||
- 1.10.2 Fix 1.10.1 release - add aiohttp support back in
|
||||
- 1.10.1 [bad release] Fix build for Fedora package + python2 (thanks @puiterwijk and @lamenezes)
|
||||
- 1.10.0 Add support for aiohttp (thanks @lamenezes)
|
||||
- 1.9.0 Add support for boto3 (thanks @desdm, @foorbarna). Fix deepcopy issue
|
||||
for response headers when `decode_compressed_response` is enabled (thanks
|
||||
@nickdirienzo)
|
||||
- 1.8.0 Fix for Serialization errors with JSON adapter (thanks
|
||||
@aliaksandrb). Avoid concatenating bytes with strings (thanks
|
||||
@jaysonsantos). Exclude __pycache__ dirs & compiled files in sdist
|
||||
(thanks @koobs). Fix Tornado support behavior for Tornado 3 (thanks
|
||||
@abhinav). decode_compressed_response option and filter (thanks
|
||||
@jayvdb).
|
||||
- 1.7.4 [#217] Make use_cassette decorated functions actually return a
|
||||
value (thanks @bcen). [#199] Fix path transfromation defaults.
|
||||
Better headers dictionary management.
|
||||
- 1.7.3 [#188] ``additional_matchers`` kwarg on ``use_cassette``.
|
||||
[#191] Actually support passing multiple before_record_request
|
||||
functions (thanks @agriffis).
|
||||
- 1.7.2 [#186] Get effective_url in tornado (thanks @mvschaik), [#187]
|
||||
Set request_time on Response object in tornado (thanks @abhinav).
|
||||
- 1.7.1 [#183] Patch ``fetch_impl`` instead of the entire HTTPClient
|
||||
class for Tornado (thanks @abhinav).
|
||||
- 1.7.0 [#177] Properly support coroutine/generator decoration. [#178]
|
||||
Support distribute (thanks @graingert). [#163] Make compatibility
|
||||
between python2 and python3 recorded cassettes more robust (thanks
|
||||
@gward).
|
||||
- 1.6.1 [#169] Support conditional requirements in old versions of
|
||||
pip, Fix RST parse errors generated by pandoc, [Tornado] Fix
|
||||
unsupported features exception not being raised, [#166]
|
||||
content-aware body matcher.
|
||||
- 1.6.0 [#120] Tornado support (thanks @abhinav), [#147] packaging fixes
|
||||
(thanks @graingert), [#158] allow filtering post params in requests
|
||||
(thanks @MrJohz), [#140] add xmlrpclib support (thanks @Diaoul).
|
||||
- 1.5.2 Fix crash when cassette path contains cassette library
|
||||
directory (thanks @gazpachoking).
|
||||
- 1.5.0 Automatic cassette naming and 'application/json' post data
|
||||
filtering (thanks @marco-santamaria).
|
||||
- 1.4.2 Fix a bug caused by requests 2.7 and chunked transfer encoding
|
||||
- 1.4.1 Include README, tests, LICENSE in package. Thanks @ralphbean.
|
||||
- 1.4.0 Filter post data parameters (thanks @eadmundo), support for
|
||||
posting files through requests, inject\_cassette kwarg to access
|
||||
cassette from ``use_cassette`` decorated function,
|
||||
``with_current_defaults`` actually works (thanks @samstav).
|
||||
- 1.3.0 Fix/add support for urllib3 (thanks @aisch), fix default port
|
||||
for https (thanks @abhinav).
|
||||
- 1.2.0 Add custom\_patches argument to VCR/Cassette objects to allow
|
||||
users to stub custom classes when cassettes become active.
|
||||
- 1.1.4 Add force reset around calls to actual connection from stubs,
|
||||
to ensure compatibility with the version of httplib/urlib2 in python
|
||||
2.7.9.
|
||||
- 1.1.3 Fix python3 headers field (thanks @rtaboada), fix boto test
|
||||
(thanks @telaviv), fix new\_episodes record mode (thanks @jashugan),
|
||||
fix Windows connectionpool stub bug (thanks @gazpachoking), add
|
||||
support for requests 2.5
|
||||
- 1.1.2 Add urllib==1.7.1 support. Make json serialize error handling
|
||||
correct Improve logging of match failures.
|
||||
- 1.1.1 Use function signature preserving ``wrapt.decorator`` to write
|
||||
the decorator version of use\_cassette in order to ensure
|
||||
compatibility with py.test fixtures and python 2. Move all request
|
||||
filtering into the ``before_record_callable``.
|
||||
- 1.1.0 Add ``before_record_response``. Fix several bugs related to the
|
||||
context management of cassettes.
|
||||
- 1.0.3: Fix an issue with requests 2.4 and make sure case sensitivity
|
||||
is consistent across python versions
|
||||
- 1.0.2: Fix an issue with requests 2.3
|
||||
- 1.0.1: Fix a bug with the new ignore requests feature and the once
|
||||
record mode
|
||||
- 1.0.0: *BACKWARDS INCOMPATIBLE*: Please see the 'upgrade' section in
|
||||
the README. Take a look at the matcher section as well, you might
|
||||
want to update your ``match_on`` settings. Add support for filtering
|
||||
sensitive data from requests, matching query strings after the order
|
||||
changes and improving the built-in matchers, (thanks to @mshytikov),
|
||||
support for ignoring requests to certain hosts, bump supported
|
||||
Python3 version to 3.4, fix some bugs with Boto support (thanks
|
||||
@marusich), fix error with URL field capitalization in README (thanks
|
||||
@simon-weber), added some log messages to help with debugging, added
|
||||
``all_played`` property on cassette (thanks @mshytikov)
|
||||
- 0.7.0: VCR.py now supports Python 3! (thanks @asundg) Also I
|
||||
refactored the stub connections quite a bit to add support for the
|
||||
putrequest and putheader calls. This version also adds support for
|
||||
httplib2 (thanks @nilp0inter). I have added a couple tests for boto
|
||||
since it is an http client in its own right. Finally, this version
|
||||
includes a fix for a bug where requests wasn't being patched properly
|
||||
(thanks @msabramo).
|
||||
- 0.6.0: Store response headers as a list since a HTTP response can
|
||||
have the same header twice (happens with set-cookie sometimes). This
|
||||
has the added benefit of preserving the order of headers. Thanks
|
||||
@smallcode for the bug report leading to this change. I have made an
|
||||
effort to ensure backwards compatibility with the old cassettes'
|
||||
header storage mechanism, but if you want to upgrade to the new
|
||||
header storage, you should delete your cassettes and re-record them.
|
||||
Also this release adds better error messages (thanks @msabramo) and
|
||||
adds support for using VCR as a decorator (thanks @smallcode for the
|
||||
motivation)
|
||||
- 0.5.0: Change the ``response_of`` method to ``responses_of`` since
|
||||
cassettes can now contain more than one response for a request. Since
|
||||
this changes the API, I'm bumping the version. Also includes 2
|
||||
bugfixes: a better error message when attempting to overwrite a
|
||||
cassette file, and a fix for a bug with requests sessions (thanks
|
||||
@msabramo)
|
||||
- 0.4.0: Change default request recording behavior for multiple
|
||||
requests. If you make the same request multiple times to the same
|
||||
URL, the response might be different each time (maybe the response
|
||||
has a timestamp in it or something), so this will make the same
|
||||
request multiple times and save them all. Then, when you are
|
||||
replaying the cassette, the responses will be played back in the same
|
||||
order in which they were received. If you were making multiple
|
||||
requests to the same URL in a cassette before version 0.4.0, you
|
||||
might need to regenerate your cassette files. Also, removes support
|
||||
for the cassette.play\_count counter API, since individual requests
|
||||
aren't unique anymore. A cassette might contain the same request
|
||||
several times. Also removes secure overwrite feature since that was
|
||||
breaking overwriting files in Windows, and fixes a bug preventing
|
||||
request's automatic body decompression from working.
|
||||
- 0.3.5: Fix compatibility with requests 2.x
|
||||
- 0.3.4: Bugfix: close file before renaming it. This fixes an issue on
|
||||
Windows. Thanks @smallcode for the fix.
|
||||
- 0.3.3: Bugfix for error message when an unreigstered custom matcher
|
||||
was used
|
||||
- 0.3.2: Fix issue with new config syntax and the ``match_on``
|
||||
parameter. Thanks, @chromy!
|
||||
- 0.3.1: Fix issue causing full paths to be sent on the HTTP request
|
||||
line.
|
||||
- 0.3.0: *Backwards incompatible release* - Added support for record
|
||||
modes, and changed the default recording behavior to the "once"
|
||||
record mode. Please see the documentation on record modes for more.
|
||||
Added support for custom request matching, and changed the default
|
||||
request matching behavior to match only on the URL and method. Also,
|
||||
improved the httplib mocking to add support for the
|
||||
``HTTPConnection.send()`` method. This means that requests won't
|
||||
actually be sent until the response is read, since I need to record
|
||||
the entire request in order to match up the appropriate response. I
|
||||
don't think this should cause any issues unless you are sending
|
||||
requests without ever loading the response (which none of the
|
||||
standard httplib wrappers do, as far as I know. Thanks to @fatuhoku
|
||||
for some of the ideas and the motivation behind this release.
|
||||
- 0.2.1: Fixed missing modules in setup.py
|
||||
- 0.2.0: Added configuration API, which lets you configure some
|
||||
settings on VCR (see the README). Also, VCR no longer saves cassettes
|
||||
if they haven't changed at all and supports JSON as well as YAML
|
||||
(thanks @sirpengi). Added amazing new skeumorphic logo, thanks
|
||||
@hairarrow.
|
||||
- 0.1.0: *backwards incompatible release - delete your old cassette
|
||||
files*: This release adds the ability to access the cassette to make
|
||||
assertions on it, as well as a major code refactor thanks to
|
||||
@dlecocq. It also fixes a couple longstanding bugs with redirects and
|
||||
HTTPS. [#3 and #4]
|
||||
- 0.0.4: If you have libyaml installed, vcrpy will use the c bindings
|
||||
instead. Speed up your tests! Thanks @dlecocq
|
||||
- 0.0.3: Add support for requests 1.2.3. Support for older versions of
|
||||
requests dropped (thanks @vitormazzi and @bryanhelmig)
|
||||
- 0.0.2: Add support for requests / urllib3
|
||||
- 0.0.1: Initial Release
|
||||
- Format code with black (@neozenith)
|
||||
- Use latest pypy3 in Travis (@hugovk)
|
||||
- Improve documentation about custom matchers (@gward)
|
||||
- Fix exception when body is empty (@keithprickett)
|
||||
- Add `pytest-recording` to the documentation as an alternative Pytest plugin (@Stranger6667)
|
||||
- Fix yarl and python3.5 version issue (@neozenith)
|
||||
- Fix header matcher for boto3 - fixes #474 (@simahawk)
|
||||
- 2.1.0
|
||||
- Add a `rewind` method to reset a cassette (thanks @khamidou)
|
||||
- New error message with more details on why the cassette failed to play a request (thanks @arthurHamon2, @neozenith)
|
||||
- Handle connect tunnel URI (thanks @jeking3)
|
||||
- Add code coverage to the project (thanks @neozenith)
|
||||
- Drop support to python 3.4
|
||||
- Add deprecation warning on python 2.7, next major release will drop python 2.7 support
|
||||
- Fix build problems on requests tests (thanks to @dunossauro)
|
||||
- Fix matching on 'body' failing when Unicode symbols are present in them (thanks @valgur)
|
||||
- Fix bugs on aiohttp integration (thanks @graingert, @steinnes, @stj, @lamenezes, @lmazuel)
|
||||
- Fix Biopython incompatibility (thanks @rishab121)
|
||||
- Fix Boto3 integration (thanks @1oglop1, @arthurHamon2)
|
||||
- 2.0.1
|
||||
- Fix bug when using vcrpy with python 3.4
|
||||
- 2.0.0
|
||||
- Support python 3.7 (fix httplib2 and urllib2, thanks @felixonmars)
|
||||
- [#356] Fixes `before_record_response` so the original response isn't changed (thanks @kgraves)
|
||||
- Fix requests stub when using proxy (thanks @samuelfekete @daneoshiga)
|
||||
- (only for aiohttp stub) Drop support to python 3.4 asyncio.coroutine (aiohttp doesn't support python it anymore)
|
||||
- Fix aiohttp stub to work with aiohttp client (thanks @stj)
|
||||
- Fix aiohttp stub to accept content type passed
|
||||
- Improve docs (thanks @adamchainz)
|
||||
|
||||
|
||||
- 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.
|
||||
- 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)
|
||||
- 1.11.1
|
||||
- Fix compatibility with newest requests and urllib3 releases
|
||||
- 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.10.5
|
||||
- Added a fix to httplib2 (thanks @carlosds730), Fix an issue with
|
||||
- aiohttp (thanks @madninja), Add missing requirement yarl (thanks @lamenezes),
|
||||
- Remove duplicate mock triple (thanks @FooBarQuaxx)
|
||||
- 1.10.4
|
||||
- Fix an issue with asyncio aiohttp (thanks @madninja)
|
||||
- 1.10.3
|
||||
- Fix some issues with asyncio and params (thanks @anovikov1984 and @lamenezes)
|
||||
- Fix some issues with cassette serialize / deserialize and empty response bodies (thanks @gRoussac and @dz0ny)
|
||||
- 1.10.2
|
||||
- Fix 1.10.1 release - add aiohttp support back in
|
||||
- 1.10.1
|
||||
- [bad release] Fix build for Fedora package + python2 (thanks @puiterwijk and @lamenezes)
|
||||
- 1.10.0
|
||||
- Add support for aiohttp (thanks @lamenezes)
|
||||
- 1.9.0
|
||||
- Add support for boto3 (thanks @desdm, @foorbarna).
|
||||
- Fix deepcopy issue for response headers when `decode_compressed_response` is enabled (thanks @nickdirienzo)
|
||||
- 1.8.0
|
||||
- Fix for Serialization errors with JSON adapter (thanks @aliaksandrb).
|
||||
- Avoid concatenating bytes with strings (thanks @jaysonsantos).
|
||||
- Exclude __pycache__ dirs & compiled files in sdist (thanks @koobs).
|
||||
- Fix Tornado support behavior for Tornado 3 (thanks @abhinav).
|
||||
- decode_compressed_response option and filter (thanks @jayvdb).
|
||||
- 1.7.4 [#217]
|
||||
- Make use_cassette decorated functions actually return a value (thanks @bcen).
|
||||
- [#199] Fix path transformation defaults.
|
||||
- Better headers dictionary management.
|
||||
- 1.7.3 [#188]
|
||||
- ``additional_matchers`` kwarg on ``use_cassette``.
|
||||
- [#191] Actually support passing multiple before_record_request functions (thanks @agriffis).
|
||||
- 1.7.2
|
||||
- [#186] Get effective_url in tornado (thanks @mvschaik)
|
||||
- [#187] Set request_time on Response object in tornado (thanks @abhinav).
|
||||
- 1.7.1
|
||||
- [#183] Patch ``fetch_impl`` instead of the entire HTTPClient class for Tornado (thanks @abhinav).
|
||||
- 1.7.0
|
||||
- [#177] Properly support coroutine/generator decoration.
|
||||
- [#178] Support distribute (thanks @graingert). [#163] Make compatibility between python2 and python3 recorded cassettes more robust (thanks @gward).
|
||||
- 1.6.1
|
||||
- [#169] Support conditional requirements in old versions of pip
|
||||
- Fix RST parse errors generated by pandoc
|
||||
- [Tornado] Fix unsupported features exception not being raised
|
||||
- [#166] content-aware body matcher.
|
||||
- 1.6.0
|
||||
- [#120] Tornado support (thanks @abhinav)
|
||||
- [#147] packaging fixes (thanks @graingert)
|
||||
- [#158] allow filtering post params in requests (thanks @MrJohz)
|
||||
- [#140] add xmlrpclib support (thanks @Diaoul).
|
||||
- 1.5.2
|
||||
- Fix crash when cassette path contains cassette library directory (thanks @gazpachoking).
|
||||
- 1.5.0
|
||||
- Automatic cassette naming and 'application/json' post data filtering (thanks @marco-santamaria).
|
||||
- 1.4.2
|
||||
- Fix a bug caused by requests 2.7 and chunked transfer encoding
|
||||
- 1.4.1
|
||||
- Include README, tests, LICENSE in package. Thanks @ralphbean.
|
||||
- 1.4.0
|
||||
- Filter post data parameters (thanks @eadmundo)
|
||||
- Support for posting files through requests, inject\_cassette kwarg to access cassette from ``use_cassette`` decorated function, ``with_current_defaults`` actually works (thanks @samstav).
|
||||
- 1.3.0
|
||||
- Fix/add support for urllib3 (thanks @aisch)
|
||||
- Fix default port for https (thanks @abhinav).
|
||||
- 1.2.0
|
||||
- Add custom\_patches argument to VCR/Cassette objects to allow users to stub custom classes when cassettes become active.
|
||||
- 1.1.4
|
||||
- Add force reset around calls to actual connection from stubs, to ensure compatibility with the version of httplib/urlib2 in python 2.7.9.
|
||||
- 1.1.3
|
||||
- Fix python3 headers field (thanks @rtaboada)
|
||||
- fix boto test (thanks @telaviv)
|
||||
- fix new\_episodes record mode (thanks @jashugan),
|
||||
- fix Windows connectionpool stub bug (thanks @gazpachoking)
|
||||
- add support for requests 2.5
|
||||
- 1.1.2
|
||||
- Add urllib==1.7.1 support.
|
||||
- Make json serialize error handling correct
|
||||
- Improve logging of match failures.
|
||||
- 1.1.1
|
||||
- Use function signature preserving ``wrapt.decorator`` to write the decorator version of use\_cassette in order to ensure compatibility with py.test fixtures and python 2.
|
||||
- Move all request filtering into the ``before_record_callable``.
|
||||
- 1.1.0
|
||||
- Add ``before_record_response``. Fix several bugs related to the context management of cassettes.
|
||||
- 1.0.3
|
||||
- Fix an issue with requests 2.4 and make sure case sensitivity is consistent across python versions
|
||||
- 1.0.2
|
||||
- Fix an issue with requests 2.3
|
||||
- 1.0.1
|
||||
- Fix a bug with the new ignore requests feature and the once record mode
|
||||
- 1.0.0
|
||||
- *BACKWARDS INCOMPATIBLE*: Please see the 'upgrade' section in the README. Take a look at the matcher section as well, you might want to update your ``match_on`` settings.
|
||||
- Add support for filtering sensitive data from requests, matching query strings after the order changes and improving the built-in matchers, (thanks to @mshytikov)
|
||||
- Support for ignoring requests to certain hosts, bump supported Python3 version to 3.4, fix some bugs with Boto support (thanks @marusich)
|
||||
- Fix error with URL field capitalization in README (thanks @simon-weber)
|
||||
- Added some log messages to help with debugging
|
||||
- Added ``all_played`` property on cassette (thanks @mshytikov)
|
||||
|
||||
- 0.7.0
|
||||
- VCR.py now supports Python 3! (thanks @asundg)
|
||||
- Also I refactored the stub connections quite a bit to add support for the putrequest and putheader calls.
|
||||
- This version also adds support for httplib2 (thanks @nilp0inter).
|
||||
- I have added a couple tests for boto since it is an http client in its own right.
|
||||
- Finally, this version includes a fix for a bug where requests wasn't being patched properly (thanks @msabramo).
|
||||
- 0.6.0
|
||||
- Store response headers as a list since a HTTP response can have the same header twice (happens with set-cookie sometimes).
|
||||
- This has the added benefit of preserving the order of headers.
|
||||
- Thanks @smallcode for the bug report leading to this change.
|
||||
- I have made an effort to ensure backwards compatibility with the old cassettes' header storage mechanism, but if you want to upgrade to the new header storage, you should delete your cassettes and re-record them.
|
||||
- Also this release adds better error messages (thanks @msabramo)
|
||||
- and adds support for using VCR as a decorator (thanks @smallcode for the motivation)
|
||||
- 0.5.0
|
||||
- Change the ``response_of`` method to ``responses_of`` since cassettes can now contain more than one response for a request.
|
||||
- Since this changes the API, I'm bumping the version.
|
||||
- Also includes 2 bugfixes:
|
||||
- a better error message when attempting to overwrite a cassette file,
|
||||
- and a fix for a bug with requests sessions (thanks @msabramo)
|
||||
- 0.4.0
|
||||
- Change default request recording behavior for multiple requests.
|
||||
- If you make the same request multiple times to the same URL, the response might be different each time (maybe the response has a timestamp in it or something), so this will make the same request multiple times and save them all.
|
||||
- Then, when you are replaying the cassette, the responses will be played back in the same order in which they were received.
|
||||
- If you were making multiple requests to the same URL in a cassette before version 0.4.0, you might need to regenerate your cassette files.
|
||||
- Also, removes support for the cassette.play\_count counter API, since individual requests aren't unique anymore.
|
||||
- A cassette might contain the same request several times.
|
||||
- Also removes secure overwrite feature since that was breaking overwriting files in Windows
|
||||
- And fixes a bug preventing request's automatic body decompression from working.
|
||||
- 0.3.5
|
||||
- Fix compatibility with requests 2.x
|
||||
- 0.3.4
|
||||
- Bugfix: close file before renaming it. This fixes an issue on Windows. Thanks @smallcode for the fix.
|
||||
- 0.3.3
|
||||
- Bugfix for error message when an unregistered custom matcher was used
|
||||
- 0.3.2
|
||||
- Fix issue with new config syntax and the ``match_on`` parameter. Thanks, @chromy!
|
||||
- 0.3.1
|
||||
- Fix issue causing full paths to be sent on the HTTP request line.
|
||||
- 0.3.0
|
||||
- *Backwards incompatible release*
|
||||
- Added support for record modes, and changed the default recording behavior to the "once" record mode. Please see the documentation on record modes for more.
|
||||
- Added support for custom request matching, and changed the default request matching behavior to match only on the URL and method.
|
||||
- Also, improved the httplib mocking to add support for the ``HTTPConnection.send()`` method.
|
||||
- This means that requests won't actually be sent until the response is read, since I need to record the entire request in order to match up the appropriate response.
|
||||
- I don't think this should cause any issues unless you are sending requests without ever loading the response (which none of the standard httplib wrappers do, as far as I know).
|
||||
- Thanks to @fatuhoku for some of the ideas and the motivation behind this release.
|
||||
- 0.2.1
|
||||
- Fixed missing modules in setup.py
|
||||
- 0.2.0
|
||||
- Added configuration API, which lets you configure some settings on VCR (see the README).
|
||||
- Also, VCR no longer saves cassettes if they haven't changed at all and supports JSON as well as YAML (thanks @sirpengi).
|
||||
- Added amazing new skeumorphic logo, thanks @hairarrow.
|
||||
- 0.1.0
|
||||
- *backwards incompatible release - delete your old cassette files*
|
||||
- This release adds the ability to access the cassette to make assertions on it
|
||||
- as well as a major code refactor thanks to @dlecocq.
|
||||
- It also fixes a couple longstanding bugs with redirects and HTTPS. [#3 and #4]
|
||||
- 0.0.4
|
||||
- If you have libyaml installed, vcrpy will use the c bindings instead. Speed up your tests! Thanks @dlecocq
|
||||
- 0.0.3
|
||||
- Add support for requests 1.2.3. Support for older versions of requests dropped (thanks @vitormazzi and @bryanhelmig)
|
||||
- 0.0.2
|
||||
- Add support for requests / urllib3
|
||||
- 0.0.1
|
||||
- Initial Release
|
||||
|
||||
|
||||
78
docs/conf.py
78
docs/conf.py
@@ -1,4 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# vcrpy documentation build configuration file, created by
|
||||
# sphinx-quickstart on Sun Sep 13 11:18:00 2015.
|
||||
@@ -12,8 +11,33 @@
|
||||
# All configuration values have a default; values that are commented out
|
||||
# serve to show the default.
|
||||
|
||||
import codecs
|
||||
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,
|
||||
# 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.
|
||||
@@ -27,7 +51,14 @@ import os
|
||||
# Add any Sphinx extension module names here, as strings. They can be
|
||||
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
|
||||
# ones.
|
||||
extensions = ["sphinx.ext.autodoc", "sphinx.ext.intersphinx", "sphinx.ext.coverage", "sphinx.ext.viewcode"]
|
||||
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.
|
||||
templates_path = ["_templates"]
|
||||
@@ -44,25 +75,25 @@ source_suffix = ".rst"
|
||||
master_doc = "index"
|
||||
|
||||
# General information about the project.
|
||||
project = u"vcrpy"
|
||||
copyright = u"2015, Kevin McCarthy"
|
||||
author = u"Kevin McCarthy"
|
||||
project = "vcrpy"
|
||||
copyright = "2015, Kevin McCarthy"
|
||||
author = "Kevin McCarthy"
|
||||
|
||||
# The version info for the project you're documenting, acts as replacement for
|
||||
# |version| and |release|, also used in various other places throughout the
|
||||
# built documents.
|
||||
#
|
||||
# The short X.Y version.
|
||||
version = "1.7.4"
|
||||
# version = "1.7.4"
|
||||
# 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
|
||||
# for a list of supported languages.
|
||||
#
|
||||
# This is also used if you do content translation via gettext catalogs.
|
||||
# Usually you set "language" from the command line for these cases.
|
||||
language = None
|
||||
language = "en"
|
||||
|
||||
# There are two options for replacing |today|: either, you set today to some
|
||||
# non-false value, then it is used:
|
||||
@@ -106,11 +137,11 @@ todo_include_todos = False
|
||||
|
||||
# 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
|
||||
if "READTHEDOCS" not in os.environ:
|
||||
import sphinx_rtd_theme
|
||||
|
||||
html_theme = "sphinx_rtd_theme"
|
||||
html_theme_path = [sphinx_rtd_theme.get_html_theme_path()]
|
||||
# if "READTHEDOCS" not in os.environ:
|
||||
# import sphinx_rtd_theme
|
||||
#
|
||||
# html_theme = "sphinx_rtd_theme"
|
||||
# html_theme_path = [sphinx_rtd_theme.get_html_theme_path()]
|
||||
|
||||
# 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
|
||||
@@ -153,6 +184,8 @@ html_static_path = ["_static"]
|
||||
|
||||
# Custom sidebar templates, maps document names to template names.
|
||||
# html_sidebars = {}
|
||||
html_sidebars = {"**": ["globaltoc.html", "relations.html", "sourcelink.html", "searchbox.html"]}
|
||||
|
||||
|
||||
# Additional templates that should be rendered to pages, maps page names to
|
||||
# template names.
|
||||
@@ -205,19 +238,21 @@ htmlhelp_basename = "vcrpydoc"
|
||||
|
||||
latex_elements = {
|
||||
# The paper size ('letterpaper' or 'a4paper').
|
||||
#'papersize': 'letterpaper',
|
||||
# 'papersize': 'letterpaper',
|
||||
# The font size ('10pt', '11pt' or '12pt').
|
||||
#'pointsize': '10pt',
|
||||
# 'pointsize': '10pt',
|
||||
# Additional stuff for the LaTeX preamble.
|
||||
#'preamble': '',
|
||||
# 'preamble': '',
|
||||
# Latex figure (float) alignment
|
||||
#'figure_align': 'htbp',
|
||||
# 'figure_align': 'htbp',
|
||||
}
|
||||
|
||||
# Grouping the document tree into LaTeX files. List of tuples
|
||||
# (source start file, target name, title,
|
||||
# author, documentclass [howto, manual, or own class]).
|
||||
latex_documents = [(master_doc, "vcrpy.tex", u"vcrpy Documentation", u"Kevin McCarthy", "manual")]
|
||||
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 title page.
|
||||
@@ -244,7 +279,7 @@ latex_documents = [(master_doc, "vcrpy.tex", u"vcrpy Documentation", u"Kevin McC
|
||||
|
||||
# One entry per manual page. List of tuples
|
||||
# (source start file, name, description, authors, manual section).
|
||||
man_pages = [(master_doc, "vcrpy", u"vcrpy Documentation", [author], 1)]
|
||||
man_pages = [(master_doc, "vcrpy", "vcrpy Documentation", [author], 1)]
|
||||
|
||||
# If true, show URL addresses after external links.
|
||||
# man_show_urls = False
|
||||
@@ -259,12 +294,12 @@ texinfo_documents = [
|
||||
(
|
||||
master_doc,
|
||||
"vcrpy",
|
||||
u"vcrpy Documentation",
|
||||
"vcrpy Documentation",
|
||||
author,
|
||||
"vcrpy",
|
||||
"One line description of project.",
|
||||
"Miscellaneous",
|
||||
)
|
||||
),
|
||||
]
|
||||
|
||||
# Documents to append as an appendix to all manuals.
|
||||
@@ -282,3 +317,4 @@ texinfo_documents = [
|
||||
|
||||
# Example configuration for intersphinx: refer to the Python standard library.
|
||||
intersphinx_mapping = {"https://docs.python.org/": None}
|
||||
html_theme = "alabaster"
|
||||
|
||||
@@ -42,7 +42,8 @@ This can be configured by changing the ``match_on`` setting.
|
||||
The following options are available :
|
||||
|
||||
- method (for example, POST or GET)
|
||||
- uri (the full URI.)
|
||||
- uri (the full URI)
|
||||
- scheme (for example, HTTP or HTTPS)
|
||||
- host (the hostname of the server receiving the request)
|
||||
- port (the port of the server receiving the request)
|
||||
- path (the path of the request)
|
||||
|
||||
@@ -1,23 +1,106 @@
|
||||
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.
|
||||
- ``python3 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
|
||||
------------------------
|
||||
|
||||
The tests are all run automatically on `Travis
|
||||
CI <https://travis-ci.org/kevin1024/vcrpy>`__, but you can also run them
|
||||
yourself using `py.test <http://pytest.org/>`__ and
|
||||
`Tox <http://tox.testrun.org/>`__. Tox will automatically run them in
|
||||
all environments VCR.py supports. The test suite is pretty big and slow,
|
||||
but you can tell tox to only run specific tests like this::
|
||||
yourself using `pytest <http://pytest.org/>`__ and
|
||||
`Tox <http://tox.testrun.org/>`__.
|
||||
|
||||
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 py27-requests -- -v -k "'test_status_code or test_gzip'"
|
||||
tox -e py37-requests -- -v --last-failed
|
||||
tox -e py38-requests -- -v -k "'test_status_code or test_gzip'"
|
||||
tox -e py38-requests -- -v --last-failed
|
||||
|
||||
This will run only tests that look like ``test_status_code`` or
|
||||
``test_gzip`` in the test suite, and only in the python 2.7 environment
|
||||
``test_gzip`` in the test suite, and only in the python 3.8 environment
|
||||
that has ``requests`` installed.
|
||||
|
||||
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.
|
||||
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/>`
|
||||
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/>`_
|
||||
in this example::
|
||||
|
||||
git clone https://github.com/pyenv/pyenv ~/.pyenv
|
||||
@@ -44,20 +127,20 @@ in this example::
|
||||
eval "$(pyenv init -)"
|
||||
|
||||
# Setup your local system tox tooling
|
||||
pip install tox tox-pyenv
|
||||
pip3 install tox tox-pyenv
|
||||
|
||||
# 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.8.0 pypy3.8
|
||||
|
||||
# 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.8.0 pypy3.8
|
||||
|
||||
# Run the whole test suite
|
||||
tox
|
||||
|
||||
# Run the whole test suite or just part of it
|
||||
tox -e lint
|
||||
tox -e py37-requests
|
||||
tox -e py38-requests
|
||||
|
||||
|
||||
Troubleshooting on MacOSX
|
||||
|
||||
@@ -31,7 +31,7 @@ which matchers didn't match. This can help you with debugging custom
|
||||
matchers.
|
||||
|
||||
CannotOverwriteExistingCassetteException
|
||||
========================================
|
||||
----------------------------------------
|
||||
|
||||
When a request failed to be found in an existing cassette,
|
||||
VCR.py tries to get the request(s) that may be similar to the one being searched.
|
||||
@@ -44,7 +44,7 @@ It can return multiple similar requests with :
|
||||
|
||||
CannotOverwriteExistingCassetteException message example :
|
||||
|
||||
.. code:: python
|
||||
.. code::
|
||||
|
||||
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.
|
||||
@@ -54,4 +54,4 @@ CannotOverwriteExistingCassetteException message example :
|
||||
Matchers succeeded : ['method', 'scheme', 'host', 'port', 'path']
|
||||
Matchers failed :
|
||||
query - assertion failure :
|
||||
[('alt', 'json'), ('maxResults', '200')] != [('alt', 'json'), ('maxResults', '500')]
|
||||
[('alt', 'json'), ('maxResults', '200')] != [('alt', 'json'), ('maxResults', '500')]
|
||||
|
||||
@@ -4,7 +4,7 @@ Contents
|
||||
========
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
:maxdepth: 3
|
||||
|
||||
installation
|
||||
usage
|
||||
@@ -15,6 +15,7 @@ Contents
|
||||
contributing
|
||||
changelog
|
||||
|
||||
|
||||
==================
|
||||
Indices and tables
|
||||
==================
|
||||
|
||||
@@ -4,13 +4,12 @@ Installation
|
||||
VCR.py is a package on `PyPI <https://pypi.python.org>`__, so you can install
|
||||
with pip::
|
||||
|
||||
pip install vcrpy
|
||||
pip3 install vcrpy
|
||||
|
||||
Compatibility
|
||||
-------------
|
||||
|
||||
VCR.py supports Python 2.7 and 3.5+, and
|
||||
`pypy <http://pypy.org>`__.
|
||||
VCR.py supports Python 3.8+, and `pypy <http://pypy.org>`__.
|
||||
|
||||
The following HTTP libraries are supported:
|
||||
|
||||
@@ -19,10 +18,11 @@ The following HTTP libraries are supported:
|
||||
- ``boto3``
|
||||
- ``http.client``
|
||||
- ``httplib2``
|
||||
- ``requests`` (both 1.x and 2.x versions)
|
||||
- ``requests`` (>=2.16.2 versions)
|
||||
- ``tornado.httpclient``
|
||||
- ``urllib2``
|
||||
- ``urllib3``
|
||||
- ``httpx``
|
||||
|
||||
Speed
|
||||
-----
|
||||
@@ -35,7 +35,7 @@ rebuilding pyyaml.
|
||||
|
||||
1. Test if pyyaml is built with libyaml. This should work::
|
||||
|
||||
python -c 'from yaml import CLoader'
|
||||
python3 -c 'from yaml import CLoader'
|
||||
|
||||
2. Install libyaml according to your Linux distribution, or using `Homebrew
|
||||
<http://mxcl.github.com/homebrew/>`__ on Mac::
|
||||
@@ -46,8 +46,8 @@ rebuilding pyyaml.
|
||||
|
||||
3. Rebuild pyyaml with libyaml::
|
||||
|
||||
pip uninstall pyyaml
|
||||
pip --no-cache-dir install pyyaml
|
||||
pip3 uninstall pyyaml
|
||||
pip3 --no-cache-dir install pyyaml
|
||||
|
||||
Upgrade
|
||||
-------
|
||||
@@ -61,7 +61,7 @@ is to simply delete your cassettes and re-record all of them. VCR.py
|
||||
also provides a migration script that attempts to upgrade your 0.x
|
||||
cassettes to the new 1.x format. To use it, run the following command::
|
||||
|
||||
python -m vcr.migration PATH
|
||||
python3 -m vcr.migration PATH
|
||||
|
||||
The PATH can be either a path to the directory with cassettes or the
|
||||
path to a single cassette.
|
||||
|
||||
2
docs/requirements.txt
Normal file
2
docs/requirements.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
sphinx<8
|
||||
sphinx_rtd_theme==1.2.2
|
||||
@@ -4,11 +4,11 @@ Usage
|
||||
.. code:: python
|
||||
|
||||
import vcr
|
||||
import urllib2
|
||||
import urllib.request
|
||||
|
||||
with vcr.use_cassette('fixtures/vcr_cassettes/synopsis.yaml'):
|
||||
response = urllib2.urlopen('http://www.iana.org/domains/reserved').read()
|
||||
assert 'Example domains' in response
|
||||
response = urllib.request.urlopen('http://www.iana.org/domains/reserved').read()
|
||||
assert b'Example domains' in response
|
||||
|
||||
Run this test once, and VCR.py will record the HTTP request to
|
||||
``fixtures/vcr_cassettes/synopsis.yaml``. Run it again, and VCR.py will
|
||||
@@ -25,8 +25,8 @@ look like this:
|
||||
|
||||
@vcr.use_cassette('fixtures/vcr_cassettes/synopsis.yaml')
|
||||
def test_iana():
|
||||
response = urllib2.urlopen('http://www.iana.org/domains/reserved').read()
|
||||
assert 'Example domains' in response
|
||||
response = urllib.request.urlopen('http://www.iana.org/domains/reserved').read()
|
||||
assert b'Example domains' in response
|
||||
|
||||
When using the decorator version of ``use_cassette``, it is possible to
|
||||
omit the path to the cassette file.
|
||||
@@ -35,8 +35,8 @@ omit the path to the cassette file.
|
||||
|
||||
@vcr.use_cassette()
|
||||
def test_iana():
|
||||
response = urllib2.urlopen('http://www.iana.org/domains/reserved').read()
|
||||
assert 'Example domains' in response
|
||||
response = urllib.request.urlopen('http://www.iana.org/domains/reserved').read()
|
||||
assert b'Example domains' in response
|
||||
|
||||
In this case, the cassette file will be given the same name as the test
|
||||
function, and it will be placed in the same directory as the file in
|
||||
@@ -92,9 +92,73 @@ all
|
||||
Unittest Integration
|
||||
--------------------
|
||||
|
||||
While it's possible to use the context manager or decorator forms with unittest,
|
||||
there's also a ``VCRTestCase`` provided separately by `vcrpy-unittest
|
||||
<https://github.com/agriffis/vcrpy-unittest>`__.
|
||||
Inherit from ``VCRTestCase`` for automatic recording and playback of HTTP
|
||||
interactions.
|
||||
|
||||
.. code:: python
|
||||
|
||||
from vcr.unittest import VCRTestCase
|
||||
import requests
|
||||
|
||||
class MyTestCase(VCRTestCase):
|
||||
def test_something(self):
|
||||
response = requests.get('http://example.com')
|
||||
|
||||
Similar to how VCR.py returns the cassette from the context manager,
|
||||
``VCRTestCase`` makes the cassette available as ``self.cassette``:
|
||||
|
||||
.. code:: python
|
||||
|
||||
self.assertEqual(len(self.cassette), 1)
|
||||
self.assertEqual(self.cassette.requests[0].uri, 'http://example.com')
|
||||
|
||||
By default cassettes will be placed in the ``cassettes`` subdirectory next to the
|
||||
test, named according to the test class and method. For example, the above test
|
||||
would read from and write to ``cassettes/MyTestCase.test_something.yaml``
|
||||
|
||||
The configuration can be modified by overriding methods on your subclass:
|
||||
``_get_vcr_kwargs``, ``_get_cassette_library_dir`` and ``_get_cassette_name``.
|
||||
To modify the ``VCR`` object after instantiation, for example to add a matcher,
|
||||
you can hook on ``_get_vcr``, for example:
|
||||
|
||||
.. code:: python
|
||||
|
||||
class MyTestCase(VCRTestCase):
|
||||
def _get_vcr(self, **kwargs):
|
||||
myvcr = super(MyTestCase, self)._get_vcr(**kwargs)
|
||||
myvcr.register_matcher('mymatcher', mymatcher)
|
||||
myvcr.match_on = ['mymatcher']
|
||||
return myvcr
|
||||
|
||||
See
|
||||
`the source
|
||||
<https://github.com/kevin1024/vcrpy/blob/master/vcr/unittest.py>`__
|
||||
for the default implementations of these methods.
|
||||
|
||||
If you implement a ``setUp`` method on your test class then make sure to call
|
||||
the parent version ``super().setUp()`` in your own in order to continue getting
|
||||
the cassettes produced.
|
||||
|
||||
VCRMixin
|
||||
~~~~~~~~
|
||||
|
||||
In case inheriting from ``VCRTestCase`` is difficult because of an existing
|
||||
class hierarchy containing tests in the base classes, inherit from ``VCRMixin``
|
||||
instead.
|
||||
|
||||
.. code:: python
|
||||
|
||||
from vcr.unittest import VCRMixin
|
||||
import requests
|
||||
import unittest
|
||||
|
||||
class MyTestMixin(VCRMixin):
|
||||
def test_something(self):
|
||||
response = requests.get(self.url)
|
||||
|
||||
class MyTestCase(MyTestMixin, unittest.TestCase):
|
||||
url = 'http://example.com'
|
||||
|
||||
|
||||
Pytest Integration
|
||||
------------------
|
||||
|
||||
@@ -1,2 +1,33 @@
|
||||
[tool.black]
|
||||
line-length=110
|
||||
|
||||
[tool.codespell]
|
||||
skip = '.git,*.pdf,*.svg,.tox'
|
||||
ignore-regex = "\\\\[fnrstv]"
|
||||
#
|
||||
# ignore-words-list = ''
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
markers = [
|
||||
"online",
|
||||
]
|
||||
|
||||
[tool.ruff]
|
||||
select = [
|
||||
"B", # flake8-bugbear
|
||||
"C4", # flake8-comprehensions
|
||||
"COM", # flake8-commas
|
||||
"E", # pycodestyle error
|
||||
"F", # pyflakes
|
||||
"I", # isort
|
||||
"ISC", # flake8-implicit-str-concat
|
||||
"PIE", # flake8-pie
|
||||
"RUF", # Ruff-specific rules
|
||||
"UP", # pyupgrade
|
||||
"W", # pycodestyle warning
|
||||
]
|
||||
line-length = 110
|
||||
target-version = "py38"
|
||||
|
||||
[tool.ruff.isort]
|
||||
known-first-party = [ "vcr" ]
|
||||
|
||||
@@ -4,4 +4,4 @@
|
||||
# If you are getting an INVOCATION ERROR for this script then there is
|
||||
# a good chance you are running on Windows.
|
||||
# You can and should use WSL for running tox on Windows when it calls bash scripts.
|
||||
REQUESTS_CA_BUNDLE=`python -m pytest_httpbin.certs` py.test $*
|
||||
REQUESTS_CA_BUNDLE=`python3 -m pytest_httpbin.certs` exec pytest "$@"
|
||||
|
||||
79
setup.py
79
setup.py
@@ -1,11 +1,31 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
import codecs
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
|
||||
from setuptools import setup, find_packages
|
||||
from setuptools import find_packages, setup
|
||||
from setuptools.command.test import test as TestCommand
|
||||
|
||||
long_description = open("README.rst", "r").read()
|
||||
long_description = open("README.rst").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):
|
||||
@@ -25,41 +45,60 @@ class PyTest(TestCommand):
|
||||
install_requires = [
|
||||
"PyYAML",
|
||||
"wrapt",
|
||||
"six>=1.5",
|
||||
'contextlib2; python_version=="2.7"',
|
||||
'mock; python_version=="2.7"',
|
||||
'yarl; python_version>="3.6"',
|
||||
'yarl<1.4; python_version=="3.5"',
|
||||
"yarl",
|
||||
# Support for urllib3 >=2 needs Python >=3.10
|
||||
# so we need to block urllib3 >=2 for Python <3.10 for now.
|
||||
# Note that vcrpy would work fine without any urllib3 around,
|
||||
# so this block and the dependency can be dropped at some point
|
||||
# in the future. For more Details:
|
||||
# https://github.com/kevin1024/vcrpy/pull/699#issuecomment-1551439663
|
||||
"urllib3 <2; python_version <'3.10'",
|
||||
]
|
||||
|
||||
excluded_packages = ["tests*"]
|
||||
if sys.version_info[0] == 2:
|
||||
excluded_packages.append("vcr.stubs.aiohttp_stubs")
|
||||
tests_require = [
|
||||
"aiohttp",
|
||||
"boto3",
|
||||
"httplib2",
|
||||
"httpx",
|
||||
"pytest",
|
||||
"pytest-aiohttp",
|
||||
"pytest-httpbin",
|
||||
"requests>=2.16.2",
|
||||
"tornado",
|
||||
# Needed to un-break httpbin 0.7.0. For httpbin >=0.7.1 and after,
|
||||
# this pin and the dependency itself can be removed, provided
|
||||
# that the related bug in httpbin has been fixed:
|
||||
# https://github.com/kevin1024/vcrpy/issues/645#issuecomment-1562489489
|
||||
# https://github.com/postmanlabs/httpbin/issues/673
|
||||
# https://github.com/postmanlabs/httpbin/pull/674
|
||||
"Werkzeug==2.0.3",
|
||||
]
|
||||
|
||||
setup(
|
||||
name="vcrpy",
|
||||
version="2.1.1",
|
||||
description=("Automatically mock your HTTP interactions to simplify and " "speed up testing"),
|
||||
version=find_version("vcr", "__init__.py"),
|
||||
description=("Automatically mock your HTTP interactions to simplify and speed up testing"),
|
||||
long_description=long_description,
|
||||
long_description_content_type="text/x-rst",
|
||||
author="Kevin McCarthy",
|
||||
author_email="me@kevinmccarthy.org",
|
||||
url="https://github.com/kevin1024/vcrpy",
|
||||
packages=find_packages(exclude=excluded_packages),
|
||||
python_requires=">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*",
|
||||
packages=find_packages(exclude=["tests*"]),
|
||||
python_requires=">=3.8",
|
||||
install_requires=install_requires,
|
||||
license="MIT",
|
||||
tests_require=["pytest", "mock", "pytest-httpbin"],
|
||||
tests_require=tests_require,
|
||||
classifiers=[
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
"Environment :: Console",
|
||||
"Intended Audience :: Developers",
|
||||
"Programming Language :: Python",
|
||||
"Programming Language :: Python :: 2",
|
||||
"Programming Language :: Python :: 2.7",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Programming Language :: Python :: 3.5",
|
||||
"Programming Language :: Python :: 3.6",
|
||||
"Programming Language :: Python :: 3.7",
|
||||
"Programming Language :: Python :: 3.8",
|
||||
"Programming Language :: Python :: 3.9",
|
||||
"Programming Language :: Python :: 3.10",
|
||||
"Programming Language :: Python :: 3.11",
|
||||
"Programming Language :: Python :: 3 :: Only",
|
||||
"Programming Language :: Python :: Implementation :: CPython",
|
||||
"Programming Language :: Python :: Implementation :: PyPy",
|
||||
"Topic :: Software Development :: Testing",
|
||||
|
||||
@@ -11,9 +11,12 @@ def assert_cassette_has_one_response(cass):
|
||||
assert cass.play_count == 1
|
||||
|
||||
|
||||
def assert_is_json(a_string):
|
||||
def assert_is_json_bytes(b: bytes):
|
||||
assert isinstance(b, bytes)
|
||||
|
||||
try:
|
||||
json.loads(a_string.decode("utf-8"))
|
||||
except Exception:
|
||||
assert False
|
||||
json.loads(b)
|
||||
except Exception as error:
|
||||
raise AssertionError() from error
|
||||
|
||||
assert True
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
import asyncio
|
||||
|
||||
import aiohttp
|
||||
from aiohttp.test_utils import TestClient
|
||||
|
||||
|
||||
async def aiohttp_request(loop, method, url, output="text", encoding="utf-8", content_type=None, **kwargs):
|
||||
|
||||
@@ -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://mockbin.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
|
||||
32
tests/integration/conftest.py
Normal file
32
tests/integration/conftest.py
Normal file
@@ -0,0 +1,32 @@
|
||||
import os
|
||||
import ssl
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.fixture(params=["https", "http"])
|
||||
def scheme(request):
|
||||
"""Fixture that returns both http and https."""
|
||||
return request.param
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mockbin(scheme):
|
||||
return scheme + "://mockbin.org"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mockbin_request_url(mockbin):
|
||||
return mockbin + "/request"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def httpbin_ssl_context():
|
||||
ssl_ca_location = os.environ["REQUESTS_CA_BUNDLE"]
|
||||
ssl_cert_location = os.environ["REQUESTS_CA_BUNDLE"].replace("cacert.pem", "cert.pem")
|
||||
ssl_key_location = os.environ["REQUESTS_CA_BUNDLE"].replace("cacert.pem", "key.pem")
|
||||
|
||||
ssl_context = ssl.create_default_context(cafile=ssl_ca_location)
|
||||
ssl_context.load_cert_chain(ssl_cert_location, ssl_key_location)
|
||||
|
||||
return ssl_context
|
||||
@@ -1,12 +1,15 @@
|
||||
import contextlib
|
||||
import logging
|
||||
import urllib.parse
|
||||
|
||||
import pytest
|
||||
|
||||
import vcr
|
||||
|
||||
asyncio = pytest.importorskip("asyncio")
|
||||
aiohttp = pytest.importorskip("aiohttp")
|
||||
|
||||
import vcr # noqa: E402
|
||||
|
||||
from .aiohttp_utils import aiohttp_app, aiohttp_request # noqa: E402
|
||||
|
||||
|
||||
@@ -32,14 +35,10 @@ def post(url, output="text", **kwargs):
|
||||
return request("POST", url, output="text", **kwargs)
|
||||
|
||||
|
||||
@pytest.fixture(params=["https", "http"])
|
||||
def scheme(request):
|
||||
"""Fixture that returns both http and https."""
|
||||
return request.param
|
||||
@pytest.mark.online
|
||||
def test_status(tmpdir, mockbin_request_url):
|
||||
url = mockbin_request_url
|
||||
|
||||
|
||||
def test_status(tmpdir, scheme):
|
||||
url = scheme + "://httpbin.org"
|
||||
with vcr.use_cassette(str(tmpdir.join("status.yaml"))):
|
||||
response, _ = get(url)
|
||||
|
||||
@@ -49,9 +48,10 @@ def test_status(tmpdir, scheme):
|
||||
assert cassette.play_count == 1
|
||||
|
||||
|
||||
@pytest.mark.online
|
||||
@pytest.mark.parametrize("auth", [None, aiohttp.BasicAuth("vcrpy", "test")])
|
||||
def test_headers(tmpdir, scheme, auth):
|
||||
url = scheme + "://httpbin.org"
|
||||
def test_headers(tmpdir, auth, mockbin_request_url):
|
||||
url = mockbin_request_url
|
||||
with vcr.use_cassette(str(tmpdir.join("headers.yaml"))):
|
||||
response, _ = get(url, auth=auth)
|
||||
|
||||
@@ -60,14 +60,16 @@ def test_headers(tmpdir, scheme, auth):
|
||||
request = cassette.requests[0]
|
||||
assert "AUTHORIZATION" in request.headers
|
||||
cassette_response, _ = get(url, auth=auth)
|
||||
assert dict(cassette_response.headers) == dict(response.headers)
|
||||
assert cassette_response.headers.items() == response.headers.items()
|
||||
assert cassette.play_count == 1
|
||||
assert "istr" not in cassette.data[0]
|
||||
assert "yarl.URL" not in cassette.data[0]
|
||||
|
||||
|
||||
def test_case_insensitive_headers(tmpdir, scheme):
|
||||
url = scheme + "://httpbin.org"
|
||||
@pytest.mark.online
|
||||
def test_case_insensitive_headers(tmpdir, mockbin_request_url):
|
||||
url = mockbin_request_url
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join("whatever.yaml"))):
|
||||
_, _ = get(url)
|
||||
|
||||
@@ -78,8 +80,10 @@ def test_case_insensitive_headers(tmpdir, scheme):
|
||||
assert cassette.play_count == 1
|
||||
|
||||
|
||||
def test_text(tmpdir, scheme):
|
||||
url = scheme + "://httpbin.org"
|
||||
@pytest.mark.online
|
||||
def test_text(tmpdir, mockbin_request_url):
|
||||
url = mockbin_request_url
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join("text.yaml"))):
|
||||
_, response_text = get(url)
|
||||
|
||||
@@ -89,8 +93,9 @@ def test_text(tmpdir, scheme):
|
||||
assert cassette.play_count == 1
|
||||
|
||||
|
||||
def test_json(tmpdir, scheme):
|
||||
url = scheme + "://httpbin.org/get"
|
||||
@pytest.mark.online
|
||||
def test_json(tmpdir, mockbin_request_url):
|
||||
url = mockbin_request_url
|
||||
headers = {"Content-Type": "application/json"}
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join("json.yaml"))):
|
||||
@@ -102,8 +107,9 @@ def test_json(tmpdir, scheme):
|
||||
assert cassette.play_count == 1
|
||||
|
||||
|
||||
def test_binary(tmpdir, scheme):
|
||||
url = scheme + "://httpbin.org/image/png"
|
||||
@pytest.mark.online
|
||||
def test_binary(tmpdir, mockbin_request_url):
|
||||
url = mockbin_request_url + "/image/png"
|
||||
with vcr.use_cassette(str(tmpdir.join("binary.yaml"))):
|
||||
_, response_binary = get(url, output="raw")
|
||||
|
||||
@@ -113,23 +119,25 @@ def test_binary(tmpdir, scheme):
|
||||
assert cassette.play_count == 1
|
||||
|
||||
|
||||
def test_stream(tmpdir, scheme):
|
||||
url = scheme + "://httpbin.org/get"
|
||||
@pytest.mark.online
|
||||
def test_stream(tmpdir, mockbin_request_url):
|
||||
url = mockbin_request_url
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join("stream.yaml"))):
|
||||
resp, body = get(url, output="raw") # Do not use stream here, as the stream is exhausted by vcr
|
||||
_, body = get(url, output="raw") # Do not use stream here, as the stream is exhausted by vcr
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join("stream.yaml"))) as cassette:
|
||||
cassette_resp, cassette_body = get(url, output="stream")
|
||||
_, cassette_body = get(url, output="stream")
|
||||
assert cassette_body == body
|
||||
assert cassette.play_count == 1
|
||||
|
||||
|
||||
@pytest.mark.online
|
||||
@pytest.mark.parametrize("body", ["data", "json"])
|
||||
def test_post(tmpdir, scheme, body, caplog):
|
||||
def test_post(tmpdir, body, caplog, mockbin_request_url):
|
||||
caplog.set_level(logging.INFO)
|
||||
data = {"key1": "value1", "key2": "value2"}
|
||||
url = scheme + "://httpbin.org/post"
|
||||
url = mockbin_request_url
|
||||
with vcr.use_cassette(str(tmpdir.join("post.yaml"))):
|
||||
_, response_json = post(url, **{body: data})
|
||||
|
||||
@@ -144,19 +152,21 @@ def test_post(tmpdir, scheme, body, caplog):
|
||||
(
|
||||
log
|
||||
for log in caplog.records
|
||||
if log.getMessage() == "<Request (POST) {}> not in cassette, sending to real server".format(url)
|
||||
if log.getMessage() == f"<Request (POST) {url}> not in cassette, sending to real server"
|
||||
),
|
||||
None,
|
||||
), "Log message not found."
|
||||
|
||||
|
||||
def test_params(tmpdir, scheme):
|
||||
url = scheme + "://httpbin.org/get"
|
||||
@pytest.mark.online
|
||||
def test_params(tmpdir, mockbin_request_url):
|
||||
url = mockbin_request_url + "?d=d"
|
||||
headers = {"Content-Type": "application/json"}
|
||||
params = {"a": 1, "b": False, "c": "c"}
|
||||
params = {"a": 1, "b": 2, "c": "c"}
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join("get.yaml"))) as cassette:
|
||||
_, response_json = get(url, output="json", params=params, headers=headers)
|
||||
assert response_json["queryString"] == {"a": "1", "b": "2", "c": "c", "d": "d"}
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join("get.yaml"))) as cassette:
|
||||
_, cassette_response_json = get(url, output="json", params=params, headers=headers)
|
||||
@@ -164,10 +174,11 @@ def test_params(tmpdir, scheme):
|
||||
assert cassette.play_count == 1
|
||||
|
||||
|
||||
def test_params_same_url_distinct_params(tmpdir, scheme):
|
||||
url = scheme + "://httpbin.org/get"
|
||||
@pytest.mark.online
|
||||
def test_params_same_url_distinct_params(tmpdir, mockbin_request_url):
|
||||
url = mockbin_request_url
|
||||
headers = {"Content-Type": "application/json"}
|
||||
params = {"a": 1, "b": False, "c": "c"}
|
||||
params = {"a": 1, "b": 2, "c": "c"}
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join("get.yaml"))) as cassette:
|
||||
_, response_json = get(url, output="json", params=params, headers=headers)
|
||||
@@ -179,13 +190,13 @@ def test_params_same_url_distinct_params(tmpdir, scheme):
|
||||
|
||||
other_params = {"other": "params"}
|
||||
with vcr.use_cassette(str(tmpdir.join("get.yaml"))) as cassette:
|
||||
response, cassette_response_text = get(url, output="text", params=other_params)
|
||||
assert "No match for the request" in cassette_response_text
|
||||
assert response.status == 599
|
||||
with pytest.raises(vcr.errors.CannotOverwriteExistingCassetteException):
|
||||
get(url, output="text", params=other_params)
|
||||
|
||||
|
||||
def test_params_on_url(tmpdir, scheme):
|
||||
url = scheme + "://httpbin.org/get?a=1&b=foo"
|
||||
@pytest.mark.online
|
||||
def test_params_on_url(tmpdir, mockbin_request_url):
|
||||
url = mockbin_request_url + "?a=1&b=foo"
|
||||
headers = {"Content-Type": "application/json"}
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join("get.yaml"))) as cassette:
|
||||
@@ -249,8 +260,9 @@ def test_aiohttp_test_client_json(aiohttp_client, tmpdir):
|
||||
assert cassette.play_count == 1
|
||||
|
||||
|
||||
def test_redirect(aiohttp_client, tmpdir):
|
||||
url = "https://httpbin.org/redirect/2"
|
||||
@pytest.mark.online
|
||||
def test_redirect(tmpdir, mockbin):
|
||||
url = mockbin + "/redirect/302/2"
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join("redirect.yaml"))):
|
||||
response, _ = get(url)
|
||||
@@ -262,3 +274,185 @@ def test_redirect(aiohttp_client, tmpdir):
|
||||
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_info.url == response.request_info.url
|
||||
assert cassette_response.request_info.method == response.request_info.method
|
||||
assert cassette_response.request_info.headers.items() == response.request_info.headers.items()
|
||||
assert cassette_response.request_info.real_url == response.request_info.real_url
|
||||
|
||||
|
||||
@pytest.mark.online
|
||||
def test_not_modified(tmpdir, mockbin):
|
||||
"""It doesn't try to redirect on 304"""
|
||||
url = mockbin + "/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
|
||||
|
||||
|
||||
@pytest.mark.online
|
||||
def test_double_requests(tmpdir, mockbin_request_url):
|
||||
"""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 = mockbin_request_url
|
||||
|
||||
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(httpbin_both, httpbin_ssl_context, tmpdir):
|
||||
async def run(loop):
|
||||
cookies_url = httpbin_both.url + (
|
||||
"/response-headers?"
|
||||
"set-cookie=" + urllib.parse.quote("cookie_1=val_1; Path=/") + "&"
|
||||
"Set-Cookie=" + urllib.parse.quote("Cookie_2=Val_2; Path=/")
|
||||
)
|
||||
home_url = httpbin_both.url + "/"
|
||||
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, cookie_jar=aiohttp.CookieJar(unsafe=True)) as session:
|
||||
cookies_resp = await session.get(cookies_url, ssl=httpbin_ssl_context)
|
||||
home_resp = await session.get(
|
||||
home_url,
|
||||
cookies=req_cookies,
|
||||
headers=req_headers,
|
||||
ssl=httpbin_ssl_context,
|
||||
)
|
||||
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, cookie_jar=aiohttp.CookieJar(unsafe=True)) as session:
|
||||
cookies_resp = await session.get(cookies_url, ssl=httpbin_ssl_context)
|
||||
home_resp = await session.get(
|
||||
home_url,
|
||||
cookies=req_cookies,
|
||||
headers=req_headers,
|
||||
ssl=httpbin_ssl_context,
|
||||
)
|
||||
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(httpbin_both, httpbin_ssl_context, tmpdir):
|
||||
async def run(loop):
|
||||
# Sets cookie as provided by the query string and redirects
|
||||
cookies_url = httpbin_both.url + "/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, cookie_jar=aiohttp.CookieJar(unsafe=True)) as session:
|
||||
cookies_resp = await session.get(cookies_url, ssl=httpbin_ssl_context)
|
||||
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
|
||||
|
||||
assert 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, cookie_jar=aiohttp.CookieJar(unsafe=True)) as session:
|
||||
cookies_resp = await session.get(cookies_url, ssl=httpbin_ssl_context)
|
||||
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
|
||||
|
||||
assert 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, cookie_jar=aiohttp.CookieJar(unsafe=True)) as session:
|
||||
cookies_resp = await session.get(cookies_url, ssl=httpbin_ssl_context)
|
||||
assert not cookies_resp.cookies
|
||||
cookies = session.cookie_jar.filter_cookies(cookies_url)
|
||||
assert cookies["Cookie_1"].value == "Val_1"
|
||||
|
||||
run_in_loop(run)
|
||||
|
||||
|
||||
@pytest.mark.online
|
||||
def test_not_allow_redirects(tmpdir, mockbin):
|
||||
url = mockbin + "/redirect/308/5"
|
||||
path = str(tmpdir.join("redirects.yaml"))
|
||||
|
||||
with vcr.use_cassette(path):
|
||||
response, _ = get(url, allow_redirects=False)
|
||||
assert response.url.path == "/redirect/308/5"
|
||||
assert response.status == 308
|
||||
|
||||
with vcr.use_cassette(path) as cassette:
|
||||
response, _ = get(url, allow_redirects=False)
|
||||
assert response.url.path == "/redirect/308/5"
|
||||
assert response.status == 308
|
||||
assert cassette.play_count == 1
|
||||
|
||||
|
||||
def test_filter_query_parameters(tmpdir, httpbin):
|
||||
url = httpbin + "?password=secret"
|
||||
path = str(tmpdir.join("query_param_filter.yaml"))
|
||||
|
||||
with vcr.use_cassette(path, filter_query_parameters=["password"]) as cassette:
|
||||
get(url)
|
||||
|
||||
assert "password" not in cassette.requests[0].url
|
||||
assert "secret" not in cassette.requests[0].url
|
||||
|
||||
with open(path) as f:
|
||||
cassette_content = f.read()
|
||||
assert "password" not in cassette_content
|
||||
assert "secret" not in cassette_content
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Basic tests for cassettes"""
|
||||
|
||||
# External imports
|
||||
import os
|
||||
from six.moves.urllib.request import urlopen
|
||||
from urllib.request import urlopen
|
||||
|
||||
# Internal imports
|
||||
import vcr
|
||||
@@ -11,7 +10,7 @@ import vcr
|
||||
|
||||
def test_nonexistent_directory(tmpdir, httpbin):
|
||||
"""If we load a cassette in a nonexistent directory, it can save ok"""
|
||||
# Check to make sure directory doesnt exist
|
||||
# Check to make sure directory doesn't exist
|
||||
assert not os.path.exists(str(tmpdir.join("nonexistent")))
|
||||
|
||||
# Run VCR to create dir and cassette file
|
||||
|
||||
@@ -1,18 +1,15 @@
|
||||
from configparser import DuplicateSectionError
|
||||
|
||||
import pytest
|
||||
|
||||
import vcr
|
||||
|
||||
boto = pytest.importorskip("boto")
|
||||
|
||||
import boto # NOQA
|
||||
import boto.iam # NOQA
|
||||
from boto.s3.connection import S3Connection # NOQA
|
||||
from boto.s3.key import Key # NOQA
|
||||
import vcr # NOQA
|
||||
|
||||
try: # NOQA
|
||||
from ConfigParser import DuplicateSectionError # NOQA
|
||||
except ImportError: # NOQA
|
||||
# python3
|
||||
from configparser import DuplicateSectionError # NOQA
|
||||
import boto # noqa
|
||||
import boto.iam # noqa
|
||||
from boto.s3.connection import S3Connection # noqa
|
||||
from boto.s3.key import Key # noqa
|
||||
|
||||
|
||||
def test_boto_stubs(tmpdir):
|
||||
@@ -20,6 +17,7 @@ def test_boto_stubs(tmpdir):
|
||||
# Perform the imports within the patched context so that
|
||||
# CertValidatingHTTPSConnection refers to the patched version.
|
||||
from boto.https_connection import CertValidatingHTTPSConnection
|
||||
|
||||
from vcr.stubs.boto_stubs import VCRCertValidatingHTTPSConnection
|
||||
|
||||
# Prove that the class was patched by the stub and that we can instantiate it.
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import pytest
|
||||
import os
|
||||
|
||||
import pytest
|
||||
|
||||
import vcr
|
||||
|
||||
boto3 = pytest.importorskip("boto3")
|
||||
|
||||
import boto3 # NOQA
|
||||
import botocore # NOQA
|
||||
import vcr # NOQA
|
||||
import botocore # noqa
|
||||
|
||||
try:
|
||||
from botocore import awsrequest # NOQA
|
||||
from botocore import awsrequest # noqa
|
||||
|
||||
botocore_awsrequest = True
|
||||
except ImportError:
|
||||
@@ -18,12 +19,12 @@ except ImportError:
|
||||
# https://github.com/boto/botocore/pull/1495
|
||||
boto3_skip_vendored_requests = pytest.mark.skipif(
|
||||
botocore_awsrequest,
|
||||
reason="botocore version {ver} does not use vendored requests anymore.".format(ver=botocore.__version__),
|
||||
reason=f"botocore version {botocore.__version__} does not use vendored requests anymore.",
|
||||
)
|
||||
|
||||
boto3_skip_awsrequest = pytest.mark.skipif(
|
||||
not botocore_awsrequest,
|
||||
reason="botocore version {ver} still uses vendored requests.".format(ver=botocore.__version__),
|
||||
reason=f"botocore version {botocore.__version__} still uses vendored requests.",
|
||||
)
|
||||
|
||||
IAM_USER_NAME = "vcrpy"
|
||||
@@ -55,24 +56,6 @@ def get_user(iam_client):
|
||||
return _get_user
|
||||
|
||||
|
||||
@boto3_skip_vendored_requests
|
||||
def test_boto_vendored_stubs(tmpdir):
|
||||
with vcr.use_cassette(str(tmpdir.join("boto3-stubs.yml"))):
|
||||
# Perform the imports within the patched context so that
|
||||
# HTTPConnection, VerifiedHTTPSConnection refers to the patched version.
|
||||
from botocore.vendored.requests.packages.urllib3.connectionpool import (
|
||||
HTTPConnection,
|
||||
VerifiedHTTPSConnection,
|
||||
)
|
||||
from vcr.stubs.boto3_stubs import VCRRequestsHTTPConnection, VCRRequestsHTTPSConnection
|
||||
|
||||
# Prove that the class was patched by the stub and that we can instantiate it.
|
||||
assert issubclass(HTTPConnection, VCRRequestsHTTPConnection)
|
||||
assert issubclass(VerifiedHTTPSConnection, VCRRequestsHTTPSConnection)
|
||||
HTTPConnection("hostname.does.not.matter")
|
||||
VerifiedHTTPSConnection("hostname.does.not.matter")
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
os.environ.get("TRAVIS_PULL_REQUEST") != "false",
|
||||
reason="Encrypted Environment Variables from Travis Repository Settings"
|
||||
@@ -80,7 +63,6 @@ def test_boto_vendored_stubs(tmpdir):
|
||||
"https://docs.travis-ci.com/user/pull-requests/#pull-requests-and-security-restrictions",
|
||||
)
|
||||
def test_boto_medium_difficulty(tmpdir, get_user):
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join("boto3-medium.yml"))):
|
||||
response = get_user()
|
||||
assert response["User"]["UserName"] == IAM_USER_NAME
|
||||
|
||||
@@ -1,50 +1,58 @@
|
||||
import os
|
||||
import json
|
||||
import os
|
||||
from urllib.request import urlopen
|
||||
|
||||
import pytest
|
||||
|
||||
import vcr
|
||||
from six.moves.urllib.request import urlopen
|
||||
|
||||
|
||||
def test_set_serializer_default_config(tmpdir, httpbin):
|
||||
@pytest.mark.online
|
||||
def test_set_serializer_default_config(tmpdir, mockbin_request_url):
|
||||
my_vcr = vcr.VCR(serializer="json")
|
||||
|
||||
with my_vcr.use_cassette(str(tmpdir.join("test.json"))):
|
||||
assert my_vcr.serializer == "json"
|
||||
urlopen(httpbin.url + "/get")
|
||||
urlopen(mockbin_request_url)
|
||||
|
||||
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):
|
||||
@pytest.mark.online
|
||||
def test_default_set_cassette_library_dir(tmpdir, mockbin_request_url):
|
||||
my_vcr = vcr.VCR(cassette_library_dir=str(tmpdir.join("subdir")))
|
||||
|
||||
with my_vcr.use_cassette("test.json"):
|
||||
urlopen(httpbin.url + "/get")
|
||||
urlopen(mockbin_request_url)
|
||||
|
||||
assert os.path.exists(str(tmpdir.join("subdir").join("test.json")))
|
||||
|
||||
|
||||
def test_override_set_cassette_library_dir(tmpdir, httpbin):
|
||||
@pytest.mark.online
|
||||
def test_override_set_cassette_library_dir(tmpdir, mockbin_request_url):
|
||||
my_vcr = vcr.VCR(cassette_library_dir=str(tmpdir.join("subdir")))
|
||||
|
||||
cld = str(tmpdir.join("subdir2"))
|
||||
|
||||
with my_vcr.use_cassette("test.json", cassette_library_dir=cld):
|
||||
urlopen(httpbin.url + "/get")
|
||||
urlopen(mockbin_request_url)
|
||||
|
||||
assert os.path.exists(str(tmpdir.join("subdir2").join("test.json")))
|
||||
assert not os.path.exists(str(tmpdir.join("subdir").join("test.json")))
|
||||
|
||||
|
||||
def test_override_match_on(tmpdir, httpbin):
|
||||
@pytest.mark.online
|
||||
def test_override_match_on(tmpdir, mockbin_request_url):
|
||||
my_vcr = vcr.VCR(match_on=["method"])
|
||||
|
||||
with my_vcr.use_cassette(str(tmpdir.join("test.json"))):
|
||||
urlopen(httpbin.url)
|
||||
urlopen(mockbin_request_url)
|
||||
|
||||
with my_vcr.use_cassette(str(tmpdir.join("test.json"))) as cass:
|
||||
urlopen(httpbin.url + "/get")
|
||||
urlopen(mockbin_request_url)
|
||||
|
||||
assert len(cass) == 1
|
||||
assert cass.play_count == 1
|
||||
@@ -56,3 +64,24 @@ def test_missing_matcher():
|
||||
with pytest.raises(KeyError):
|
||||
with my_vcr.use_cassette("test.yaml", match_on=["notawesome"]):
|
||||
pass
|
||||
|
||||
|
||||
@pytest.mark.online
|
||||
def test_dont_record_on_exception(tmpdir, mockbin_request_url):
|
||||
my_vcr = vcr.VCR(record_on_exception=False)
|
||||
|
||||
@my_vcr.use_cassette(str(tmpdir.join("dontsave.yml")))
|
||||
def some_test():
|
||||
assert b"Not in content" in urlopen(mockbin_request_url)
|
||||
|
||||
with pytest.raises(AssertionError):
|
||||
some_test()
|
||||
|
||||
assert not os.path.exists(str(tmpdir.join("dontsave.yml")))
|
||||
|
||||
# Make sure context decorator has the same behavior
|
||||
with pytest.raises(AssertionError):
|
||||
with my_vcr.use_cassette(str(tmpdir.join("dontsave2.yml"))):
|
||||
assert b"Not in content" in urlopen(mockbin_request_url).read()
|
||||
|
||||
assert not os.path.exists(str(tmpdir.join("dontsave2.yml")))
|
||||
|
||||
@@ -1,28 +1,30 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Basic tests about save behavior"""
|
||||
|
||||
# External imports
|
||||
import os
|
||||
import time
|
||||
from six.moves.urllib.request import urlopen
|
||||
from urllib.request import urlopen
|
||||
|
||||
import pytest
|
||||
|
||||
# Internal imports
|
||||
import vcr
|
||||
|
||||
|
||||
def test_disk_saver_nowrite(tmpdir, httpbin):
|
||||
@pytest.mark.online
|
||||
def test_disk_saver_nowrite(tmpdir, mockbin_request_url):
|
||||
"""
|
||||
Ensure that when you close a cassette without changing it it doesn't
|
||||
rewrite the file
|
||||
"""
|
||||
fname = str(tmpdir.join("synopsis.yaml"))
|
||||
with vcr.use_cassette(fname) as cass:
|
||||
urlopen(httpbin.url).read()
|
||||
urlopen(mockbin_request_url).read()
|
||||
assert cass.play_count == 0
|
||||
last_mod = os.path.getmtime(fname)
|
||||
|
||||
with vcr.use_cassette(fname) as cass:
|
||||
urlopen(httpbin.url).read()
|
||||
urlopen(mockbin_request_url).read()
|
||||
assert cass.play_count == 1
|
||||
assert cass.dirty is False
|
||||
last_mod2 = os.path.getmtime(fname)
|
||||
@@ -30,14 +32,15 @@ def test_disk_saver_nowrite(tmpdir, httpbin):
|
||||
assert last_mod == last_mod2
|
||||
|
||||
|
||||
def test_disk_saver_write(tmpdir, httpbin):
|
||||
@pytest.mark.online
|
||||
def test_disk_saver_write(tmpdir, mockbin_request_url):
|
||||
"""
|
||||
Ensure that when you close a cassette after changing it it does
|
||||
rewrite the file
|
||||
"""
|
||||
fname = str(tmpdir.join("synopsis.yaml"))
|
||||
with vcr.use_cassette(fname) as cass:
|
||||
urlopen(httpbin.url).read()
|
||||
urlopen(mockbin_request_url).read()
|
||||
assert cass.play_count == 0
|
||||
last_mod = os.path.getmtime(fname)
|
||||
|
||||
@@ -45,9 +48,9 @@ def test_disk_saver_write(tmpdir, httpbin):
|
||||
# the mtime doesn't change
|
||||
time.sleep(1)
|
||||
|
||||
with vcr.use_cassette(fname, record_mode="any") as cass:
|
||||
urlopen(httpbin.url).read()
|
||||
urlopen(httpbin.url + "/get").read()
|
||||
with vcr.use_cassette(fname, record_mode=vcr.mode.ANY) as cass:
|
||||
urlopen(mockbin_request_url).read()
|
||||
urlopen(mockbin_request_url + "/get").read()
|
||||
assert cass.play_count == 1
|
||||
assert cass.dirty
|
||||
last_mod2 = os.path.getmtime(fname)
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import base64
|
||||
import pytest
|
||||
from six.moves.urllib.request import urlopen, Request
|
||||
from six.moves.urllib.parse import urlencode
|
||||
from six.moves.urllib.error import HTTPError
|
||||
import vcr
|
||||
import json
|
||||
from assertions import assert_cassette_has_one_response, assert_is_json
|
||||
from urllib.error import HTTPError
|
||||
from urllib.parse import urlencode
|
||||
from urllib.request import Request, urlopen
|
||||
|
||||
import pytest
|
||||
from assertions import assert_cassette_has_one_response, assert_is_json_bytes
|
||||
|
||||
import vcr
|
||||
|
||||
|
||||
def _request_with_auth(url, username, password):
|
||||
@@ -43,13 +45,18 @@ def test_filter_basic_auth(tmpdir, httpbin):
|
||||
|
||||
|
||||
def test_filter_querystring(tmpdir, httpbin):
|
||||
url = httpbin.url + "/?foo=bar"
|
||||
url = httpbin.url + "/?password=secret"
|
||||
cass_file = str(tmpdir.join("filter_qs.yaml"))
|
||||
with vcr.use_cassette(cass_file, filter_query_parameters=["foo"]):
|
||||
with vcr.use_cassette(cass_file, filter_query_parameters=["password"]):
|
||||
urlopen(url)
|
||||
with vcr.use_cassette(cass_file, filter_query_parameters=["foo"]) as cass:
|
||||
with vcr.use_cassette(cass_file, filter_query_parameters=["password"]) as cass:
|
||||
urlopen(url)
|
||||
assert "foo" not in cass.requests[0].url
|
||||
assert "password" not in cass.requests[0].url
|
||||
assert "secret" not in cass.requests[0].url
|
||||
with open(cass_file) as f:
|
||||
cassette_content = f.read()
|
||||
assert "password" not in cassette_content
|
||||
assert "secret" not in cassette_content
|
||||
|
||||
|
||||
def test_filter_post_data(tmpdir, httpbin):
|
||||
@@ -103,7 +110,19 @@ def test_decompress_gzip(tmpdir, httpbin):
|
||||
with vcr.use_cassette(cass_file) as cass:
|
||||
decoded_response = urlopen(url).read()
|
||||
assert_cassette_has_one_response(cass)
|
||||
assert_is_json(decoded_response)
|
||||
assert_is_json_bytes(decoded_response)
|
||||
|
||||
|
||||
def test_decomptess_empty_body(tmpdir, httpbin):
|
||||
url = httpbin.url + "/gzip"
|
||||
request = Request(url, headers={"Accept-Encoding": ["gzip, deflate"]}, method="HEAD")
|
||||
cass_file = str(tmpdir.join("gzip_empty_response.yaml"))
|
||||
with vcr.use_cassette(cass_file, decode_compressed_response=True):
|
||||
response = urlopen(request).read()
|
||||
with vcr.use_cassette(cass_file) as cass:
|
||||
decoded_response = urlopen(request).read()
|
||||
assert_cassette_has_one_response(cass)
|
||||
assert decoded_response == response
|
||||
|
||||
|
||||
def test_decompress_deflate(tmpdir, httpbin):
|
||||
@@ -115,7 +134,7 @@ def test_decompress_deflate(tmpdir, httpbin):
|
||||
with vcr.use_cassette(cass_file) as cass:
|
||||
decoded_response = urlopen(url).read()
|
||||
assert_cassette_has_one_response(cass)
|
||||
assert_is_json(decoded_response)
|
||||
assert_is_json_bytes(decoded_response)
|
||||
|
||||
|
||||
def test_decompress_regular(tmpdir, httpbin):
|
||||
@@ -127,4 +146,25 @@ def test_decompress_regular(tmpdir, httpbin):
|
||||
with vcr.use_cassette(cass_file) as cass:
|
||||
resp = urlopen(url).read()
|
||||
assert_cassette_has_one_response(cass)
|
||||
assert_is_json(resp)
|
||||
assert_is_json_bytes(resp)
|
||||
|
||||
|
||||
def test_before_record_request_corruption(tmpdir, httpbin):
|
||||
"""Modifying request in before_record_request should not affect outgoing request"""
|
||||
|
||||
def before_record(request):
|
||||
request.headers.clear()
|
||||
request.body = b""
|
||||
return request
|
||||
|
||||
req = Request(
|
||||
httpbin.url + "/post",
|
||||
data=urlencode({"test": "exists"}).encode(),
|
||||
headers={"X-Test": "exists"},
|
||||
)
|
||||
cass_file = str(tmpdir.join("modified_response.yaml"))
|
||||
with vcr.use_cassette(cass_file, before_record_request=before_record):
|
||||
resp = json.loads(urlopen(req).read())
|
||||
|
||||
assert resp["headers"]["X-Test"] == "exists"
|
||||
assert resp["form"]["test"] == "exists"
|
||||
|
||||
@@ -1,16 +1,12 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Integration tests with httplib2"""
|
||||
from urllib.parse import urlencode
|
||||
|
||||
import sys
|
||||
|
||||
from six.moves.urllib_parse import urlencode
|
||||
import pytest
|
||||
import pytest_httpbin.certs
|
||||
from assertions import assert_cassette_has_one_response
|
||||
|
||||
import vcr
|
||||
|
||||
from assertions import assert_cassette_has_one_response
|
||||
|
||||
httplib2 = pytest.importorskip("httplib2")
|
||||
|
||||
|
||||
@@ -20,8 +16,6 @@ def http():
|
||||
with the certificate replaced by the httpbin one.
|
||||
"""
|
||||
kwargs = {"ca_certs": pytest_httpbin.certs.where()}
|
||||
if sys.version_info[:2] in [(2, 7), (3, 7)]:
|
||||
kwargs["disable_ssl_certificate_validation"] = True
|
||||
return httplib2.Http(**kwargs)
|
||||
|
||||
|
||||
@@ -61,13 +55,15 @@ def test_response_headers(tmpdir, httpbin_both):
|
||||
assert set(headers) == set(resp.items())
|
||||
|
||||
|
||||
def test_effective_url(tmpdir, httpbin_both):
|
||||
@pytest.mark.online
|
||||
def test_effective_url(tmpdir):
|
||||
"""Ensure that the effective_url is captured"""
|
||||
url = httpbin_both.url + "/redirect-to?url=/html"
|
||||
url = "http://mockbin.org/redirect/301"
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join("headers.yaml"))):
|
||||
resp, _ = http().request(url)
|
||||
effective_url = resp["content-location"]
|
||||
assert effective_url == httpbin_both + "/html"
|
||||
assert effective_url == "http://mockbin.org/redirect/301/0"
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join("headers.yaml"))):
|
||||
resp, _ = http().request(url)
|
||||
@@ -111,7 +107,7 @@ def test_post_data(tmpdir, httpbin_both):
|
||||
|
||||
def test_post_unicode_data(tmpdir, httpbin_both):
|
||||
"""Ensure that it works when posting unicode data"""
|
||||
data = urlencode({"snowman": u"☃".encode("utf-8")})
|
||||
data = urlencode({"snowman": "☃".encode()})
|
||||
url = httpbin_both.url + "/post"
|
||||
with vcr.use_cassette(str(tmpdir.join("post_data.yaml"))):
|
||||
_, res1 = http().request(url, "POST", data)
|
||||
|
||||
316
tests/integration/test_httpx.py
Normal file
316
tests/integration/test_httpx.py
Normal file
@@ -0,0 +1,316 @@
|
||||
import os
|
||||
|
||||
import pytest
|
||||
|
||||
import vcr
|
||||
|
||||
asyncio = pytest.importorskip("asyncio")
|
||||
httpx = pytest.importorskip("httpx")
|
||||
|
||||
from vcr.stubs.httpx_stubs import HTTPX_REDIRECT_PARAM # noqa: E402
|
||||
|
||||
|
||||
class BaseDoRequest:
|
||||
_client_class = None
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self._client_args = args
|
||||
self._client_kwargs = kwargs
|
||||
|
||||
def _make_client(self):
|
||||
return self._client_class(*self._client_args, **self._client_kwargs)
|
||||
|
||||
|
||||
class DoSyncRequest(BaseDoRequest):
|
||||
_client_class = httpx.Client
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, *args):
|
||||
pass
|
||||
|
||||
@property
|
||||
def client(self):
|
||||
try:
|
||||
return self._client
|
||||
except AttributeError:
|
||||
self._client = self._make_client()
|
||||
return self._client
|
||||
|
||||
def __call__(self, *args, **kwargs):
|
||||
return self.client.request(*args, timeout=60, **kwargs)
|
||||
|
||||
|
||||
class DoAsyncRequest(BaseDoRequest):
|
||||
_client_class = httpx.AsyncClient
|
||||
|
||||
def __enter__(self):
|
||||
# Need to manage both loop and client, because client's implementation
|
||||
# will fail if the loop is closed before the client's end of life.
|
||||
self._loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(self._loop)
|
||||
self._client = self._make_client()
|
||||
self._loop.run_until_complete(self._client.__aenter__())
|
||||
return self
|
||||
|
||||
def __exit__(self, *args):
|
||||
try:
|
||||
self._loop.run_until_complete(self._client.__aexit__(*args))
|
||||
finally:
|
||||
del self._client
|
||||
self._loop.close()
|
||||
del self._loop
|
||||
|
||||
@property
|
||||
def client(self):
|
||||
try:
|
||||
return self._client
|
||||
except AttributeError as e:
|
||||
raise ValueError('To access async client, use "with do_request() as client"') from e
|
||||
|
||||
def __call__(self, *args, **kwargs):
|
||||
if hasattr(self, "_loop"):
|
||||
return self._loop.run_until_complete(self.client.request(*args, **kwargs))
|
||||
|
||||
# Use one-time context and dispose of the loop/client afterwards
|
||||
with self:
|
||||
return self(*args, **kwargs)
|
||||
|
||||
|
||||
def pytest_generate_tests(metafunc):
|
||||
if "do_request" in metafunc.fixturenames:
|
||||
metafunc.parametrize("do_request", [DoAsyncRequest, DoSyncRequest])
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def yml(tmpdir, request):
|
||||
return str(tmpdir.join(request.function.__name__ + ".yaml"))
|
||||
|
||||
|
||||
@pytest.mark.online
|
||||
def test_status(tmpdir, mockbin, do_request):
|
||||
url = mockbin
|
||||
|
||||
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
|
||||
|
||||
|
||||
@pytest.mark.online
|
||||
def test_case_insensitive_headers(tmpdir, mockbin, do_request):
|
||||
url = mockbin
|
||||
|
||||
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
|
||||
|
||||
|
||||
@pytest.mark.online
|
||||
def test_content(tmpdir, mockbin, do_request):
|
||||
url = mockbin
|
||||
|
||||
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
|
||||
|
||||
|
||||
@pytest.mark.online
|
||||
def test_json(tmpdir, mockbin, do_request):
|
||||
url = mockbin + "/request"
|
||||
|
||||
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
|
||||
|
||||
|
||||
@pytest.mark.online
|
||||
def test_params_same_url_distinct_params(tmpdir, mockbin, do_request):
|
||||
url = mockbin + "/request"
|
||||
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)
|
||||
|
||||
|
||||
@pytest.mark.online
|
||||
def test_redirect(mockbin, yml, do_request):
|
||||
url = mockbin + "/redirect/303/2"
|
||||
|
||||
redirect_kwargs = {HTTPX_REDIRECT_PARAM.name: True}
|
||||
|
||||
response = do_request()("GET", url, **redirect_kwargs)
|
||||
with vcr.use_cassette(yml):
|
||||
response = do_request()("GET", url, **redirect_kwargs)
|
||||
|
||||
with vcr.use_cassette(yml) as cassette:
|
||||
cassette_response = do_request()("GET", url, **redirect_kwargs)
|
||||
|
||||
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 cassette_response.request.headers.items() == response.request.headers.items()
|
||||
|
||||
|
||||
@pytest.mark.online
|
||||
def test_work_with_gzipped_data(mockbin, do_request, yml):
|
||||
url = mockbin + "/gzip?foo=bar"
|
||||
headers = {"accept-encoding": "deflate, gzip"}
|
||||
|
||||
with vcr.use_cassette(yml):
|
||||
do_request(headers=headers)("GET", url)
|
||||
|
||||
with vcr.use_cassette(yml) as cassette:
|
||||
cassette_response = do_request(headers=headers)("GET", url)
|
||||
|
||||
assert cassette_response.headers["content-encoding"] == "gzip"
|
||||
assert cassette_response.read()
|
||||
assert cassette.play_count == 1
|
||||
|
||||
|
||||
@pytest.mark.online
|
||||
@pytest.mark.parametrize("url", ["https://github.com/kevin1024/vcrpy/issues/" + str(i) for i in range(3, 6)])
|
||||
def test_simple_fetching(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://mockbin.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
|
||||
|
||||
|
||||
@pytest.mark.online
|
||||
def test_cookies(tmpdir, mockbin, do_request):
|
||||
def client_cookies(client):
|
||||
return list(client.client.cookies)
|
||||
|
||||
def response_cookies(response):
|
||||
return list(response.cookies)
|
||||
|
||||
url = mockbin + "/bin/26148652-fe25-4f21-aaf5-689b5b4bf65f"
|
||||
headers = {"cookie": "k1=v1;k2=v2"}
|
||||
|
||||
with do_request(headers=headers) as client:
|
||||
assert client_cookies(client) == []
|
||||
|
||||
redirect_kwargs = {HTTPX_REDIRECT_PARAM.name: True}
|
||||
|
||||
testfile = str(tmpdir.join("cookies.yml"))
|
||||
with vcr.use_cassette(testfile):
|
||||
r1 = client("GET", url, **redirect_kwargs)
|
||||
|
||||
assert response_cookies(r1) == ["k1", "k2"]
|
||||
|
||||
r2 = client("GET", url, **redirect_kwargs)
|
||||
|
||||
assert response_cookies(r2) == ["k1", "k2"]
|
||||
assert client_cookies(client) == ["k1", "k2"]
|
||||
|
||||
with do_request(headers=headers) as new_client:
|
||||
assert client_cookies(new_client) == []
|
||||
|
||||
with vcr.use_cassette(testfile) as cassette:
|
||||
cassette_response = new_client("GET", url)
|
||||
|
||||
assert cassette.play_count == 1
|
||||
assert response_cookies(cassette_response) == ["k1", "k2"]
|
||||
assert client_cookies(new_client) == ["k1", "k2"]
|
||||
|
||||
|
||||
@pytest.mark.online
|
||||
def test_relative_redirects(tmpdir, scheme, do_request, mockbin):
|
||||
redirect_kwargs = {HTTPX_REDIRECT_PARAM.name: True}
|
||||
|
||||
url = mockbin + "/redirect/301?to=/redirect/301?to=/request"
|
||||
testfile = str(tmpdir.join("relative_redirects.yml"))
|
||||
with vcr.use_cassette(testfile):
|
||||
response = do_request()("GET", url, **redirect_kwargs)
|
||||
assert len(response.history) == 2, response
|
||||
assert response.json()["url"].endswith("request")
|
||||
|
||||
with vcr.use_cassette(testfile) as cassette:
|
||||
response = do_request()("GET", url, **redirect_kwargs)
|
||||
assert len(response.history) == 2
|
||||
assert response.json()["url"].endswith("request")
|
||||
|
||||
assert cassette.play_count == 3
|
||||
|
||||
|
||||
@pytest.mark.online
|
||||
def test_redirect_wo_allow_redirects(do_request, mockbin, yml):
|
||||
url = mockbin + "/redirect/308/5"
|
||||
|
||||
redirect_kwargs = {HTTPX_REDIRECT_PARAM.name: False}
|
||||
|
||||
with vcr.use_cassette(yml):
|
||||
response = do_request()("GET", url, **redirect_kwargs)
|
||||
|
||||
assert str(response.url).endswith("308/5")
|
||||
assert response.status_code == 308
|
||||
|
||||
with vcr.use_cassette(yml) as cassette:
|
||||
response = do_request()("GET", url, **redirect_kwargs)
|
||||
|
||||
assert str(response.url).endswith("308/5")
|
||||
assert response.status_code == 308
|
||||
|
||||
assert cassette.play_count == 1
|
||||
@@ -1,6 +1,7 @@
|
||||
from six.moves.urllib.request import urlopen
|
||||
import socket
|
||||
from contextlib import contextmanager
|
||||
from urllib.request import urlopen
|
||||
|
||||
import vcr
|
||||
|
||||
|
||||
@@ -27,9 +28,9 @@ def test_ignore_localhost(tmpdir, httpbin):
|
||||
with overridden_dns({"httpbin.org": "127.0.0.1"}):
|
||||
cass_file = str(tmpdir.join("filter_qs.yaml"))
|
||||
with vcr.use_cassette(cass_file, ignore_localhost=True) as cass:
|
||||
urlopen("http://localhost:{}/".format(httpbin.port))
|
||||
urlopen(f"http://localhost:{httpbin.port}/")
|
||||
assert len(cass) == 0
|
||||
urlopen("http://httpbin.org:{}/".format(httpbin.port))
|
||||
urlopen(f"http://httpbin.org:{httpbin.port}/")
|
||||
assert len(cass) == 1
|
||||
|
||||
|
||||
@@ -37,9 +38,9 @@ def test_ignore_httpbin(tmpdir, httpbin):
|
||||
with overridden_dns({"httpbin.org": "127.0.0.1"}):
|
||||
cass_file = str(tmpdir.join("filter_qs.yaml"))
|
||||
with vcr.use_cassette(cass_file, ignore_hosts=["httpbin.org"]) as cass:
|
||||
urlopen("http://httpbin.org:{}/".format(httpbin.port))
|
||||
urlopen(f"http://httpbin.org:{httpbin.port}/")
|
||||
assert len(cass) == 0
|
||||
urlopen("http://localhost:{}/".format(httpbin.port))
|
||||
urlopen(f"http://localhost:{httpbin.port}/")
|
||||
assert len(cass) == 1
|
||||
|
||||
|
||||
@@ -47,8 +48,8 @@ def test_ignore_localhost_and_httpbin(tmpdir, httpbin):
|
||||
with overridden_dns({"httpbin.org": "127.0.0.1"}):
|
||||
cass_file = str(tmpdir.join("filter_qs.yaml"))
|
||||
with vcr.use_cassette(cass_file, ignore_hosts=["httpbin.org"], ignore_localhost=True) as cass:
|
||||
urlopen("http://httpbin.org:{}".format(httpbin.port))
|
||||
urlopen("http://localhost:{}".format(httpbin.port))
|
||||
urlopen(f"http://httpbin.org:{httpbin.port}")
|
||||
urlopen(f"http://localhost:{httpbin.port}")
|
||||
assert len(cass) == 0
|
||||
|
||||
|
||||
@@ -56,12 +57,12 @@ def test_ignore_localhost_twice(tmpdir, httpbin):
|
||||
with overridden_dns({"httpbin.org": "127.0.0.1"}):
|
||||
cass_file = str(tmpdir.join("filter_qs.yaml"))
|
||||
with vcr.use_cassette(cass_file, ignore_localhost=True) as cass:
|
||||
urlopen("http://localhost:{}".format(httpbin.port))
|
||||
urlopen(f"http://localhost:{httpbin.port}")
|
||||
assert len(cass) == 0
|
||||
urlopen("http://httpbin.org:{}".format(httpbin.port))
|
||||
urlopen(f"http://httpbin.org:{httpbin.port}")
|
||||
assert len(cass) == 1
|
||||
with vcr.use_cassette(cass_file, ignore_localhost=True) as cass:
|
||||
assert len(cass) == 1
|
||||
urlopen("http://localhost:{}".format(httpbin.port))
|
||||
urlopen("http://httpbin.org:{}".format(httpbin.port))
|
||||
urlopen(f"http://localhost:{httpbin.port}")
|
||||
urlopen(f"http://httpbin.org:{httpbin.port}")
|
||||
assert len(cass) == 1
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import vcr
|
||||
import pytest
|
||||
from six.moves.urllib.request import urlopen
|
||||
from urllib.request import urlopen
|
||||
|
||||
import pytest
|
||||
|
||||
import vcr
|
||||
|
||||
DEFAULT_URI = "http://httpbin.org/get?p1=q1&p2=q2" # base uri for testing
|
||||
|
||||
@@ -13,13 +14,13 @@ def _replace_httpbin(uri, httpbin, httpbin_secure):
|
||||
@pytest.fixture
|
||||
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
|
||||
"""
|
||||
default_uri = _replace_httpbin(DEFAULT_URI, httpbin, httpbin_secure)
|
||||
|
||||
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)
|
||||
return cassette_path
|
||||
|
||||
@@ -35,7 +36,6 @@ def cassette(tmpdir, httpbin, httpbin_secure):
|
||||
],
|
||||
)
|
||||
def test_matchers(httpbin, httpbin_secure, cassette, matcher, matching_uri, not_matching_uri):
|
||||
|
||||
matching_uri = _replace_httpbin(matching_uri, httpbin, httpbin_secure)
|
||||
not_matching_uri = _replace_httpbin(not_matching_uri, httpbin, httpbin_secure)
|
||||
default_uri = _replace_httpbin(DEFAULT_URI, httpbin, httpbin_secure)
|
||||
@@ -72,10 +72,14 @@ def test_method_matcher(cassette, httpbin, httpbin_secure):
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"uri", [DEFAULT_URI, "http://httpbin.org/get?p2=q2&p1=q1", "http://httpbin.org/get?p2=q2&p1=q1"]
|
||||
"uri",
|
||||
(
|
||||
DEFAULT_URI,
|
||||
"http://httpbin.org/get?p2=q2&p1=q1",
|
||||
"http://httpbin.org/get?p2=q2&p1=q1",
|
||||
),
|
||||
)
|
||||
def test_default_matcher_matches(cassette, uri, httpbin, httpbin_secure):
|
||||
|
||||
uri = _replace_httpbin(uri, httpbin, httpbin_secure)
|
||||
|
||||
with vcr.use_cassette(cassette) as cass:
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
from urllib.request import urlopen
|
||||
|
||||
import pytest
|
||||
|
||||
import vcr
|
||||
from six.moves.urllib.request import urlopen
|
||||
from vcr.errors import CannotOverwriteExistingCassetteException
|
||||
|
||||
|
||||
def test_making_extra_request_raises_exception(tmpdir, httpbin):
|
||||
@@ -16,5 +19,5 @@ def test_making_extra_request_raises_exception(tmpdir, httpbin):
|
||||
with vcr.use_cassette(str(tmpdir.join("test.json")), match_on=["method"]):
|
||||
assert urlopen(httpbin.url + "/status/200").getcode() == 200
|
||||
assert urlopen(httpbin.url + "/status/201").getcode() == 201
|
||||
with pytest.raises(Exception):
|
||||
with pytest.raises(CannotOverwriteExistingCassetteException):
|
||||
urlopen(httpbin.url + "/status/200")
|
||||
|
||||
@@ -1,21 +1,19 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Test using a proxy."""
|
||||
|
||||
# External imports
|
||||
import http.server
|
||||
import multiprocessing
|
||||
import socketserver
|
||||
from urllib.request import urlopen
|
||||
|
||||
import pytest
|
||||
|
||||
from six.moves import socketserver, SimpleHTTPServer
|
||||
from six.moves.urllib.request import urlopen
|
||||
|
||||
# Internal imports
|
||||
import vcr
|
||||
|
||||
# Conditional imports
|
||||
requests = pytest.importorskip("requests")
|
||||
|
||||
|
||||
class Proxy(SimpleHTTPServer.SimpleHTTPRequestHandler):
|
||||
class Proxy(http.server.SimpleHTTPRequestHandler):
|
||||
"""
|
||||
Simple proxy server.
|
||||
|
||||
@@ -38,7 +36,7 @@ class Proxy(SimpleHTTPServer.SimpleHTTPRequestHandler):
|
||||
self.copyfile(upstream_response, self.wfile)
|
||||
|
||||
|
||||
@pytest.yield_fixture(scope="session")
|
||||
@pytest.fixture(scope="session")
|
||||
def proxy_server():
|
||||
httpd = socketserver.ThreadingTCPServer(("", 0), Proxy)
|
||||
proxy_process = multiprocessing.Process(target=httpd.serve_forever)
|
||||
|
||||
@@ -1,33 +1,36 @@
|
||||
from urllib.request import urlopen
|
||||
|
||||
import pytest
|
||||
|
||||
import vcr
|
||||
from six.moves.urllib.request import urlopen
|
||||
from vcr.errors import CannotOverwriteExistingCassetteException
|
||||
|
||||
|
||||
def test_once_record_mode(tmpdir, httpbin):
|
||||
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.
|
||||
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
|
||||
urlopen(httpbin.url).read()
|
||||
|
||||
# the first time, it's played from the cassette.
|
||||
# but, try to access something else from the same cassette, and an
|
||||
# exception is raised.
|
||||
with pytest.raises(Exception):
|
||||
with pytest.raises(CannotOverwriteExistingCassetteException):
|
||||
urlopen(httpbin.url + "/get").read()
|
||||
|
||||
|
||||
def test_once_record_mode_two_times(tmpdir, httpbin):
|
||||
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
|
||||
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
|
||||
urlopen(httpbin.url).read()
|
||||
urlopen(httpbin.url).read()
|
||||
@@ -35,7 +38,7 @@ def test_once_record_mode_two_times(tmpdir, httpbin):
|
||||
|
||||
def test_once_mode_three_times(tmpdir, httpbin):
|
||||
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
|
||||
urlopen(httpbin.url).read()
|
||||
urlopen(httpbin.url).read()
|
||||
@@ -45,11 +48,11 @@ def test_once_mode_three_times(tmpdir, httpbin):
|
||||
def test_new_episodes_record_mode(tmpdir, httpbin):
|
||||
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.
|
||||
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
|
||||
urlopen(httpbin.url).read()
|
||||
|
||||
@@ -57,7 +60,7 @@ def test_new_episodes_record_mode(tmpdir, httpbin):
|
||||
assert cass.all_played
|
||||
|
||||
# in the "new_episodes" record mode, we can add more requests to
|
||||
# a cassette without repurcussions.
|
||||
# a cassette without repercussions.
|
||||
urlopen(httpbin.url + "/get").read()
|
||||
|
||||
# one of the responses has been played
|
||||
@@ -66,7 +69,7 @@ def test_new_episodes_record_mode(tmpdir, httpbin):
|
||||
# not all responses have been 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
|
||||
assert len(cass.responses) == 2
|
||||
|
||||
@@ -74,11 +77,11 @@ def test_new_episodes_record_mode(tmpdir, httpbin):
|
||||
def test_new_episodes_record_mode_two_times(tmpdir, httpbin):
|
||||
testfile = str(tmpdir.join("recordmode.yml"))
|
||||
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.
|
||||
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
|
||||
assert urlopen(url).read() == original_first_response
|
||||
|
||||
@@ -86,29 +89,29 @@ def test_new_episodes_record_mode_two_times(tmpdir, httpbin):
|
||||
# to the cassette without repercussions
|
||||
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
|
||||
assert urlopen(url).read() == original_first_response
|
||||
assert urlopen(url).read() == original_second_response
|
||||
# now that we are back in once mode, this should raise
|
||||
# an error.
|
||||
with pytest.raises(Exception):
|
||||
with pytest.raises(CannotOverwriteExistingCassetteException):
|
||||
urlopen(url).read()
|
||||
|
||||
|
||||
def test_all_record_mode(tmpdir, httpbin):
|
||||
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.
|
||||
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
|
||||
urlopen(httpbin.url).read()
|
||||
|
||||
# in the "all" record mode, we can add more requests to
|
||||
# a cassette without repurcussions.
|
||||
# a cassette without repercussions.
|
||||
urlopen(httpbin.url + "/get").read()
|
||||
|
||||
# The cassette was never actually played, even though it existed.
|
||||
@@ -121,8 +124,8 @@ def test_none_record_mode(tmpdir, httpbin):
|
||||
# Cassette file doesn't exist, yet we are trying to make a request.
|
||||
# raise hell.
|
||||
testfile = str(tmpdir.join("recordmode.yml"))
|
||||
with vcr.use_cassette(testfile, record_mode="none"):
|
||||
with pytest.raises(Exception):
|
||||
with vcr.use_cassette(testfile, record_mode=vcr.mode.NONE):
|
||||
with pytest.raises(CannotOverwriteExistingCassetteException):
|
||||
urlopen(httpbin.url).read()
|
||||
|
||||
|
||||
@@ -130,13 +133,13 @@ def test_none_record_mode_with_existing_cassette(tmpdir, httpbin):
|
||||
# create a cassette file
|
||||
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()
|
||||
|
||||
# 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()
|
||||
assert cass.play_count == 1
|
||||
# but if I try to hit the net, raise an exception.
|
||||
with pytest.raises(Exception):
|
||||
with pytest.raises(CannotOverwriteExistingCassetteException):
|
||||
urlopen(httpbin.url + "/get").read()
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
from urllib.request import urlopen
|
||||
|
||||
import pytest
|
||||
|
||||
import vcr
|
||||
from six.moves.urllib.request import urlopen
|
||||
|
||||
|
||||
def true_matcher(r1, r2):
|
||||
@@ -10,27 +13,29 @@ def false_matcher(r1, r2):
|
||||
return False
|
||||
|
||||
|
||||
def test_registered_true_matcher(tmpdir, httpbin):
|
||||
@pytest.mark.online
|
||||
def test_registered_true_matcher(tmpdir, mockbin_request_url):
|
||||
my_vcr = vcr.VCR()
|
||||
my_vcr.register_matcher("true", true_matcher)
|
||||
testfile = str(tmpdir.join("test.yml"))
|
||||
with my_vcr.use_cassette(testfile, match_on=["true"]):
|
||||
# These 2 different urls are stored as the same request
|
||||
urlopen(httpbin.url)
|
||||
urlopen(httpbin.url + "/get")
|
||||
urlopen(mockbin_request_url)
|
||||
urlopen(mockbin_request_url + "/get")
|
||||
|
||||
with my_vcr.use_cassette(testfile, match_on=["true"]):
|
||||
# I can get the response twice even though I only asked for it once
|
||||
urlopen(httpbin.url + "/get")
|
||||
urlopen(httpbin.url + "/get")
|
||||
urlopen(mockbin_request_url)
|
||||
urlopen(mockbin_request_url)
|
||||
|
||||
|
||||
def test_registered_false_matcher(tmpdir, httpbin):
|
||||
@pytest.mark.online
|
||||
def test_registered_false_matcher(tmpdir, mockbin_request_url):
|
||||
my_vcr = vcr.VCR()
|
||||
my_vcr.register_matcher("false", false_matcher)
|
||||
testfile = str(tmpdir.join("test.yml"))
|
||||
with my_vcr.use_cassette(testfile, match_on=["false"]) as cass:
|
||||
# These 2 different urls are stored as different requests
|
||||
urlopen(httpbin.url)
|
||||
urlopen(httpbin.url + "/get")
|
||||
urlopen(mockbin_request_url)
|
||||
urlopen(mockbin_request_url + "/get")
|
||||
assert len(cass) == 2
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Tests for cassettes with custom persistence"""
|
||||
|
||||
# External imports
|
||||
import os
|
||||
from six.moves.urllib.request import urlopen
|
||||
from urllib.request import urlopen
|
||||
|
||||
import pytest
|
||||
|
||||
# Internal imports
|
||||
import vcr
|
||||
from vcr.persisters.filesystem import FilesystemPersister
|
||||
from vcr.persisters.filesystem import CassetteDecodeError, CassetteNotFoundError, FilesystemPersister
|
||||
|
||||
|
||||
class CustomFilesystemPersister(object):
|
||||
class CustomFilesystemPersister:
|
||||
"""Behaves just like default FilesystemPersister but adds .test extension
|
||||
to the cassette file"""
|
||||
to the cassette file"""
|
||||
|
||||
@staticmethod
|
||||
def load_cassette(cassette_path, serializer):
|
||||
@@ -25,12 +26,25 @@ class CustomFilesystemPersister(object):
|
||||
FilesystemPersister.save_cassette(cassette_path, cassette_dict, serializer)
|
||||
|
||||
|
||||
class BadPersister(FilesystemPersister):
|
||||
"""A bad persister that raises different errors."""
|
||||
|
||||
@staticmethod
|
||||
def load_cassette(cassette_path, serializer):
|
||||
if "nonexistent" in cassette_path:
|
||||
raise CassetteNotFoundError()
|
||||
elif "encoding" in cassette_path:
|
||||
raise CassetteDecodeError()
|
||||
else:
|
||||
raise ValueError("buggy persister")
|
||||
|
||||
|
||||
def test_save_cassette_with_custom_persister(tmpdir, httpbin):
|
||||
"""Ensure you can save a cassette using custom persister"""
|
||||
my_vcr = vcr.VCR()
|
||||
my_vcr.register_persister(CustomFilesystemPersister)
|
||||
|
||||
# Check to make sure directory doesnt exist
|
||||
# Check to make sure directory doesn't exist
|
||||
assert not os.path.exists(str(tmpdir.join("nonexistent")))
|
||||
|
||||
# Run VCR to create dir and cassette file using new save_cassette callback
|
||||
@@ -53,3 +67,22 @@ def test_load_cassette_with_custom_persister(tmpdir, httpbin):
|
||||
with my_vcr.use_cassette(test_fixture, serializer="json"):
|
||||
response = urlopen(httpbin.url).read()
|
||||
assert b"difficult sometimes" in response
|
||||
|
||||
|
||||
def test_load_cassette_persister_exception_handling(tmpdir, httpbin):
|
||||
"""
|
||||
Ensure expected errors from persister are swallowed while unexpected ones
|
||||
are passed up the call stack.
|
||||
"""
|
||||
my_vcr = vcr.VCR()
|
||||
my_vcr.register_persister(BadPersister)
|
||||
|
||||
with my_vcr.use_cassette("bad/nonexistent") as cass:
|
||||
assert len(cass) == 0
|
||||
|
||||
with my_vcr.use_cassette("bad/encoding") as cass:
|
||||
assert len(cass) == 0
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
with my_vcr.use_cassette("bad/buggy") as cass:
|
||||
pass
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import vcr
|
||||
|
||||
|
||||
class MockSerializer(object):
|
||||
class MockSerializer:
|
||||
def __init__(self):
|
||||
self.serialize_count = 0
|
||||
self.deserialize_count = 0
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from urllib.request import urlopen
|
||||
|
||||
import vcr
|
||||
from six.moves.urllib.request import urlopen
|
||||
|
||||
|
||||
def test_recorded_request_uri_with_redirected_request(tmpdir, httpbin):
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Test requests' interaction with vcr"""
|
||||
import platform
|
||||
import pytest
|
||||
import sys
|
||||
from assertions import assert_cassette_empty, assert_is_json_bytes
|
||||
|
||||
import vcr
|
||||
from assertions import assert_cassette_empty, assert_is_json
|
||||
|
||||
requests = pytest.importorskip("requests")
|
||||
from requests.exceptions import ConnectionError # noqa E402
|
||||
|
||||
|
||||
def test_status_code(httpbin_both, tmpdir):
|
||||
@@ -116,26 +113,6 @@ def test_post_chunked_binary(tmpdir, httpbin):
|
||||
assert req1 == req2
|
||||
|
||||
|
||||
@pytest.mark.skipif("sys.version_info >= (3, 6)", strict=True, raises=ConnectionError)
|
||||
@pytest.mark.skipif(
|
||||
(3, 5) < sys.version_info < (3, 6) and platform.python_implementation() == "CPython",
|
||||
reason="Fails on CPython 3.5",
|
||||
)
|
||||
def test_post_chunked_binary_secure(tmpdir, httpbin_secure):
|
||||
"""Ensure that we can send chunked binary without breaking while trying to concatenate bytes with str."""
|
||||
data1 = iter([b"data", b"to", b"send"])
|
||||
data2 = iter([b"data", b"to", b"send"])
|
||||
url = httpbin_secure.url + "/post"
|
||||
with vcr.use_cassette(str(tmpdir.join("requests.yaml"))):
|
||||
req1 = requests.post(url, data1).content
|
||||
print(req1)
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join("requests.yaml"))):
|
||||
req2 = requests.post(url, data2).content
|
||||
|
||||
assert req1 == req2
|
||||
|
||||
|
||||
def test_redirects(tmpdir, httpbin_both):
|
||||
"""Ensure that we can handle redirects"""
|
||||
url = httpbin_both + "/redirect-to?url=bytes/1024"
|
||||
@@ -150,6 +127,17 @@ def test_redirects(tmpdir, httpbin_both):
|
||||
assert cass.play_count == 2
|
||||
|
||||
|
||||
def test_raw_stream(tmpdir, httpbin):
|
||||
expected_response = requests.get(httpbin.url, stream=True)
|
||||
expected_content = b"".join(expected_response.raw.stream())
|
||||
|
||||
for _ in range(2): # one for recording, one for cassette reply
|
||||
with vcr.use_cassette(str(tmpdir.join("raw_stream.yaml"))):
|
||||
actual_response = requests.get(httpbin.url, stream=True)
|
||||
actual_content = b"".join(actual_response.raw.stream())
|
||||
assert actual_content == expected_content
|
||||
|
||||
|
||||
def test_cross_scheme(tmpdir, httpbin_secure, httpbin):
|
||||
"""Ensure that requests between schemes are treated separately"""
|
||||
# First fetch a url under http, and then again under https and then
|
||||
@@ -162,20 +150,41 @@ def test_cross_scheme(tmpdir, httpbin_secure, httpbin):
|
||||
assert len(cass) == 2
|
||||
|
||||
|
||||
def test_gzip(tmpdir, httpbin_both):
|
||||
def test_gzip__decode_compressed_response_false(tmpdir, httpbin_both):
|
||||
"""
|
||||
Ensure that requests (actually urllib3) is able to automatically decompress
|
||||
the response body
|
||||
"""
|
||||
for _ in range(2): # one for recording, one for re-playing
|
||||
with vcr.use_cassette(str(tmpdir.join("gzip.yaml"))):
|
||||
response = requests.get(httpbin_both + "/gzip")
|
||||
assert response.headers["content-encoding"] == "gzip" # i.e. not removed
|
||||
assert_is_json_bytes(response.content) # i.e. uncompressed bytes
|
||||
|
||||
|
||||
def test_gzip__decode_compressed_response_true(tmpdir, httpbin_both):
|
||||
url = httpbin_both + "/gzip"
|
||||
response = requests.get(url)
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join("gzip.yaml"))):
|
||||
response = requests.get(url)
|
||||
assert_is_json(response.content)
|
||||
expected_response = requests.get(url)
|
||||
expected_content = expected_response.content
|
||||
assert expected_response.headers["content-encoding"] == "gzip" # self-test
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join("gzip.yaml"))):
|
||||
assert_is_json(response.content)
|
||||
with vcr.use_cassette(
|
||||
str(tmpdir.join("decode_compressed.yaml")),
|
||||
decode_compressed_response=True,
|
||||
) as cassette:
|
||||
r = requests.get(url)
|
||||
assert r.headers["content-encoding"] == "gzip" # i.e. not removed
|
||||
assert r.content == expected_content
|
||||
|
||||
# Has the cassette body been decompressed?
|
||||
cassette_response_body = cassette.responses[0]["body"]["string"]
|
||||
assert isinstance(cassette_response_body, str)
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join("decode_compressed.yaml")), decode_compressed_response=True):
|
||||
r = requests.get(url)
|
||||
assert "content-encoding" not in r.headers # i.e. removed
|
||||
assert r.content == expected_content
|
||||
|
||||
|
||||
def test_session_and_connection_close(tmpdir, httpbin):
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import vcr
|
||||
import zlib
|
||||
import http.client as httplib
|
||||
import json
|
||||
import six.moves.http_client as httplib
|
||||
import zlib
|
||||
|
||||
from assertions import assert_is_json
|
||||
from assertions import assert_is_json_bytes
|
||||
|
||||
import vcr
|
||||
|
||||
|
||||
def _headers_are_case_insensitive(host, port):
|
||||
@@ -64,7 +65,7 @@ def test_original_decoded_response_is_not_modified(tmpdir, httpbin):
|
||||
inside = conn.getresponse()
|
||||
|
||||
# Assert that we do not modify the original response while appending
|
||||
# to the casssette.
|
||||
# to the cassette.
|
||||
assert "gzip" == inside.headers["content-encoding"]
|
||||
|
||||
# They should effectively be the same response.
|
||||
@@ -83,7 +84,7 @@ def test_original_decoded_response_is_not_modified(tmpdir, httpbin):
|
||||
inside = conn.getresponse()
|
||||
|
||||
assert "content-encoding" not in inside.headers
|
||||
assert_is_json(inside.read())
|
||||
assert_is_json_bytes(inside.read())
|
||||
|
||||
|
||||
def _make_before_record_response(fields, replacement="[REDACTED]"):
|
||||
@@ -119,8 +120,8 @@ def test_original_response_is_not_modified_by_before_filter(tmpdir, httpbin):
|
||||
|
||||
# The scrubbed field should be the same, because no cassette existed.
|
||||
# Furthermore, the responses should be identical.
|
||||
inside_body = json.loads(inside.read().decode("utf-8"))
|
||||
outside_body = json.loads(outside.read().decode("utf-8"))
|
||||
inside_body = json.loads(inside.read())
|
||||
outside_body = json.loads(outside.read())
|
||||
assert not inside_body[field_to_scrub] == replacement
|
||||
assert inside_body[field_to_scrub] == outside_body[field_to_scrub]
|
||||
|
||||
@@ -130,5 +131,5 @@ def test_original_response_is_not_modified_by_before_filter(tmpdir, httpbin):
|
||||
conn.request("GET", "/get")
|
||||
inside = conn.getresponse()
|
||||
|
||||
inside_body = json.loads(inside.read().decode("utf-8"))
|
||||
inside_body = json.loads(inside.read())
|
||||
assert inside_body[field_to_scrub] == replacement
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Test requests' interaction with vcr"""
|
||||
|
||||
import json
|
||||
|
||||
import pytest
|
||||
from assertions import assert_cassette_empty, assert_is_json_bytes
|
||||
|
||||
import vcr
|
||||
from vcr.errors import CannotOverwriteExistingCassetteException
|
||||
|
||||
from assertions import assert_cassette_empty, assert_is_json
|
||||
|
||||
tornado = pytest.importorskip("tornado")
|
||||
http = pytest.importorskip("tornado.httpclient")
|
||||
|
||||
@@ -44,12 +43,6 @@ def post(client, url, data=None, **kwargs):
|
||||
return client.fetch(http.HTTPRequest(url, method="POST", **kwargs))
|
||||
|
||||
|
||||
@pytest.fixture(params=["https", "http"])
|
||||
def scheme(request):
|
||||
"""Fixture that returns both http and https."""
|
||||
return request.param
|
||||
|
||||
|
||||
@pytest.mark.gen_test
|
||||
def test_status_code(get_client, scheme, tmpdir):
|
||||
"""Ensure that we can read the status code"""
|
||||
@@ -90,10 +83,10 @@ def test_body(get_client, tmpdir, scheme):
|
||||
@pytest.mark.gen_test
|
||||
def test_effective_url(get_client, scheme, tmpdir):
|
||||
"""Ensure that the effective_url is captured"""
|
||||
url = scheme + "://httpbin.org/redirect-to?url=/html"
|
||||
url = scheme + "://mockbin.org/redirect/301?url=/html"
|
||||
with vcr.use_cassette(str(tmpdir.join("url.yaml"))):
|
||||
effective_url = (yield get(get_client(), url)).effective_url
|
||||
assert effective_url == scheme + "://httpbin.org/html"
|
||||
assert effective_url == scheme + "://mockbin.org/redirect/301/0"
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join("url.yaml"))) as cass:
|
||||
assert effective_url == (yield get(get_client(), url)).effective_url
|
||||
@@ -156,7 +149,7 @@ def test_post(get_client, tmpdir, scheme):
|
||||
@pytest.mark.gen_test
|
||||
def test_redirects(get_client, tmpdir, scheme):
|
||||
"""Ensure that we can handle redirects"""
|
||||
url = scheme + "://httpbin.org/redirect-to?url=bytes/1024"
|
||||
url = scheme + "://mockbin.org/redirect/301?url=bytes/1024"
|
||||
with vcr.use_cassette(str(tmpdir.join("requests.yaml"))):
|
||||
content = (yield get(get_client(), url)).body
|
||||
|
||||
@@ -201,11 +194,11 @@ def test_gzip(get_client, tmpdir, scheme):
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join("gzip.yaml"))):
|
||||
response = yield get(get_client(), url, **kwargs)
|
||||
assert_is_json(response.body)
|
||||
assert_is_json_bytes(response.body)
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join("gzip.yaml"))) as cass:
|
||||
response = yield get(get_client(), url, **kwargs)
|
||||
assert_is_json(response.body)
|
||||
assert_is_json_bytes(response.body)
|
||||
assert 1 == cass.play_count
|
||||
|
||||
|
||||
@@ -227,7 +220,7 @@ def test_unsupported_features_raises_in_future(get_client, tmpdir):
|
||||
supported is raised inside the future."""
|
||||
|
||||
def callback(chunk):
|
||||
assert False, "Did not expect to be called."
|
||||
raise AssertionError("Did not expect to be called.")
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join("invalid.yaml"))):
|
||||
future = get(get_client(), "http://httpbin.org", streaming_callback=callback)
|
||||
@@ -245,11 +238,14 @@ def test_unsupported_features_raise_error_disabled(get_client, tmpdir):
|
||||
supported is not raised if raise_error=False."""
|
||||
|
||||
def callback(chunk):
|
||||
assert False, "Did not expect to be called."
|
||||
raise AssertionError("Did not expect to be called.")
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join("invalid.yaml"))):
|
||||
response = yield get(
|
||||
get_client(), "http://httpbin.org", streaming_callback=callback, raise_error=False
|
||||
get_client(),
|
||||
"http://httpbin.org",
|
||||
streaming_callback=callback,
|
||||
raise_error=False,
|
||||
)
|
||||
|
||||
assert "not yet supported by VCR" in str(response.error)
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Integration tests with urllib2"""
|
||||
|
||||
import ssl
|
||||
from six.moves.urllib.request import urlopen
|
||||
from six.moves.urllib_parse import urlencode
|
||||
from urllib.parse import urlencode
|
||||
from urllib.request import urlopen
|
||||
|
||||
import pytest_httpbin.certs
|
||||
from assertions import assert_cassette_has_one_response
|
||||
from pytest import mark
|
||||
|
||||
# Internal imports
|
||||
import vcr
|
||||
|
||||
from assertions import assert_cassette_has_one_response
|
||||
|
||||
|
||||
def urlopen_with_cafile(*args, **kwargs):
|
||||
context = ssl.create_default_context(cafile=pytest_httpbin.certs.where())
|
||||
@@ -56,12 +56,14 @@ def test_response_headers(httpbin_both, tmpdir):
|
||||
assert sorted(open1) == sorted(open2)
|
||||
|
||||
|
||||
def test_effective_url(httpbin_both, tmpdir):
|
||||
@mark.online
|
||||
def test_effective_url(tmpdir):
|
||||
"""Ensure that the effective_url is captured"""
|
||||
url = httpbin_both.url + "/redirect-to?url=/html"
|
||||
url = "http://mockbin.org/redirect/301"
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join("headers.yaml"))):
|
||||
effective_url = urlopen_with_cafile(url).geturl()
|
||||
assert effective_url == httpbin_both.url + "/html"
|
||||
assert effective_url == "http://mockbin.org/redirect/301/0"
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join("headers.yaml"))):
|
||||
assert effective_url == urlopen_with_cafile(url).geturl()
|
||||
@@ -104,7 +106,7 @@ def test_post_data(httpbin_both, tmpdir):
|
||||
|
||||
def test_post_unicode_data(httpbin_both, tmpdir):
|
||||
"""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"
|
||||
with vcr.use_cassette(str(tmpdir.join("post_data.yaml"))):
|
||||
res1 = urlopen_with_cafile(url, data).read()
|
||||
|
||||
@@ -4,9 +4,11 @@
|
||||
|
||||
import pytest
|
||||
import pytest_httpbin
|
||||
from assertions import assert_cassette_empty, assert_is_json_bytes
|
||||
|
||||
import vcr
|
||||
from vcr.patch import force_reset
|
||||
from assertions import assert_cassette_empty, assert_is_json
|
||||
from vcr.stubs.compat import get_headers
|
||||
|
||||
urllib3 = pytest.importorskip("urllib3")
|
||||
|
||||
@@ -14,7 +16,8 @@ urllib3 = pytest.importorskip("urllib3")
|
||||
@pytest.fixture(scope="module")
|
||||
def verify_pool_mgr():
|
||||
return urllib3.PoolManager(
|
||||
cert_reqs="CERT_REQUIRED", ca_certs=pytest_httpbin.certs.where() # Force certificate check.
|
||||
cert_reqs="CERT_REQUIRED",
|
||||
ca_certs=pytest_httpbin.certs.where(), # Force certificate check.
|
||||
)
|
||||
|
||||
|
||||
@@ -40,7 +43,8 @@ def test_headers(tmpdir, httpbin_both, verify_pool_mgr):
|
||||
headers = verify_pool_mgr.request("GET", url).headers
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join("headers.yaml"))):
|
||||
assert headers == verify_pool_mgr.request("GET", url).headers
|
||||
new_headers = verify_pool_mgr.request("GET", url).headers
|
||||
assert sorted(get_headers(headers)) == sorted(get_headers(new_headers))
|
||||
|
||||
|
||||
def test_body(tmpdir, httpbin_both, verify_pool_mgr):
|
||||
@@ -94,9 +98,11 @@ def test_post(tmpdir, httpbin_both, verify_pool_mgr):
|
||||
assert req1 == req2
|
||||
|
||||
|
||||
def test_redirects(tmpdir, httpbin_both, verify_pool_mgr):
|
||||
@pytest.mark.online
|
||||
def test_redirects(tmpdir, verify_pool_mgr):
|
||||
"""Ensure that we can handle redirects"""
|
||||
url = httpbin_both.url + "/redirect-to?url=bytes/1024"
|
||||
url = "http://mockbin.org/redirect/301"
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join("verify_pool_mgr.yaml"))):
|
||||
content = verify_pool_mgr.request("GET", url).data
|
||||
|
||||
@@ -104,8 +110,9 @@ def test_redirects(tmpdir, httpbin_both, verify_pool_mgr):
|
||||
assert content == verify_pool_mgr.request("GET", url).data
|
||||
# Ensure that we've now cached *two* responses. One for the redirect
|
||||
# and one for the final fetch
|
||||
assert len(cass) == 2
|
||||
assert cass.play_count == 2
|
||||
|
||||
assert len(cass) == 2
|
||||
assert cass.play_count == 2
|
||||
|
||||
|
||||
def test_cross_scheme(tmpdir, httpbin, httpbin_secure, verify_pool_mgr):
|
||||
@@ -130,10 +137,10 @@ def test_gzip(tmpdir, httpbin_both, verify_pool_mgr):
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join("gzip.yaml"))):
|
||||
response = verify_pool_mgr.request("GET", url)
|
||||
assert_is_json(response.data)
|
||||
assert_is_json_bytes(response.data)
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join("gzip.yaml"))):
|
||||
assert_is_json(response.data)
|
||||
assert_is_json_bytes(response.data)
|
||||
|
||||
|
||||
def test_https_with_cert_validation_disabled(tmpdir, httpbin_secure, pool_mgr):
|
||||
@@ -142,18 +149,18 @@ def test_https_with_cert_validation_disabled(tmpdir, httpbin_secure, pool_mgr):
|
||||
|
||||
|
||||
def test_urllib3_force_reset():
|
||||
cpool = urllib3.connectionpool
|
||||
http_original = cpool.HTTPConnection
|
||||
https_original = cpool.HTTPSConnection
|
||||
verified_https_original = cpool.VerifiedHTTPSConnection
|
||||
conn = urllib3.connection
|
||||
http_original = conn.HTTPConnection
|
||||
https_original = conn.HTTPSConnection
|
||||
verified_https_original = conn.VerifiedHTTPSConnection
|
||||
with vcr.use_cassette(path="test"):
|
||||
first_cassette_HTTPConnection = cpool.HTTPConnection
|
||||
first_cassette_HTTPSConnection = cpool.HTTPSConnection
|
||||
first_cassette_VerifiedHTTPSConnection = cpool.VerifiedHTTPSConnection
|
||||
first_cassette_HTTPConnection = conn.HTTPConnection
|
||||
first_cassette_HTTPSConnection = conn.HTTPSConnection
|
||||
first_cassette_VerifiedHTTPSConnection = conn.VerifiedHTTPSConnection
|
||||
with force_reset():
|
||||
assert cpool.HTTPConnection is http_original
|
||||
assert cpool.HTTPSConnection is https_original
|
||||
assert cpool.VerifiedHTTPSConnection is verified_https_original
|
||||
assert cpool.HTTPConnection is first_cassette_HTTPConnection
|
||||
assert cpool.HTTPSConnection is first_cassette_HTTPSConnection
|
||||
assert cpool.VerifiedHTTPSConnection is first_cassette_VerifiedHTTPSConnection
|
||||
assert conn.HTTPConnection is http_original
|
||||
assert conn.HTTPSConnection is https_original
|
||||
assert conn.VerifiedHTTPSConnection is verified_https_original
|
||||
assert conn.HTTPConnection is first_cassette_HTTPConnection
|
||||
assert conn.HTTPSConnection is first_cassette_HTTPSConnection
|
||||
assert conn.VerifiedHTTPSConnection is first_cassette_VerifiedHTTPSConnection
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
import http.client as httplib
|
||||
import multiprocessing
|
||||
from xmlrpc.client import ServerProxy
|
||||
from xmlrpc.server import SimpleXMLRPCServer
|
||||
|
||||
import pytest
|
||||
from six.moves import xmlrpc_client, xmlrpc_server
|
||||
|
||||
import vcr
|
||||
|
||||
requests = pytest.importorskip("requests")
|
||||
|
||||
import vcr # NOQA
|
||||
|
||||
try:
|
||||
import httplib
|
||||
except ImportError:
|
||||
import http.client as httplib
|
||||
|
||||
|
||||
def test_domain_redirect():
|
||||
"""Ensure that redirects across domains are considered unique"""
|
||||
@@ -54,6 +52,7 @@ def test_flickr_multipart_upload(httpbin, tmpdir):
|
||||
assert cass.play_count == 1
|
||||
|
||||
|
||||
@pytest.mark.online
|
||||
def test_flickr_should_respond_with_200(tmpdir):
|
||||
testfile = str(tmpdir.join("flickr.yml"))
|
||||
with vcr.use_cassette(testfile):
|
||||
@@ -66,11 +65,13 @@ def test_cookies(tmpdir, httpbin):
|
||||
with vcr.use_cassette(testfile):
|
||||
s = requests.Session()
|
||||
s.get(httpbin.url + "/cookies/set?k1=v1&k2=v2")
|
||||
assert s.cookies.keys() == ["k1", "k2"]
|
||||
|
||||
r2 = s.get(httpbin.url + "/cookies")
|
||||
assert len(r2.json()["cookies"]) == 2
|
||||
assert sorted(r2.json()["cookies"].keys()) == ["k1", "k2"]
|
||||
|
||||
|
||||
@pytest.mark.online
|
||||
def test_amazon_doctype(tmpdir):
|
||||
# amazon gzips its homepage. For some reason, in requests 2.7, it's not
|
||||
# getting gunzipped.
|
||||
@@ -80,13 +81,13 @@ def test_amazon_doctype(tmpdir):
|
||||
|
||||
|
||||
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)
|
||||
q.put("http://{}:{}".format(*httpd.server_address))
|
||||
httpd.serve_forever()
|
||||
|
||||
|
||||
@pytest.yield_fixture(scope="session")
|
||||
@pytest.fixture(scope="session")
|
||||
def rpc_server():
|
||||
q = multiprocessing.Queue()
|
||||
proxy_process = multiprocessing.Process(target=start_rpc_server, args=(q,))
|
||||
@@ -99,11 +100,11 @@ def rpc_server():
|
||||
|
||||
def test_xmlrpclib(tmpdir, rpc_server):
|
||||
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)
|
||||
|
||||
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)
|
||||
|
||||
assert original_schema == second_schema
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import contextlib
|
||||
import copy
|
||||
import http.client as httplib
|
||||
import inspect
|
||||
import os
|
||||
from unittest import mock
|
||||
|
||||
from six.moves import http_client as httplib
|
||||
import pytest
|
||||
import yaml
|
||||
|
||||
from vcr.compat import mock, contextlib
|
||||
from vcr.cassette import Cassette
|
||||
from vcr.errors import UnhandledHTTPRequestError
|
||||
from vcr.patch import force_reset
|
||||
@@ -19,15 +20,31 @@ def test_cassette_load(tmpdir):
|
||||
yaml.dump(
|
||||
{
|
||||
"interactions": [
|
||||
{"request": {"body": "", "uri": "foo", "method": "GET", "headers": {}}, "response": "bar"}
|
||||
]
|
||||
}
|
||||
)
|
||||
{
|
||||
"request": {"body": "", "uri": "foo", "method": "GET", "headers": {}},
|
||||
"response": "bar",
|
||||
},
|
||||
],
|
||||
},
|
||||
),
|
||||
)
|
||||
a_cassette = Cassette.load(path=str(a_file))
|
||||
assert len(a_cassette) == 1
|
||||
|
||||
|
||||
def test_cassette_load_nonexistent():
|
||||
a_cassette = Cassette.load(path="something/nonexistent.yml")
|
||||
assert len(a_cassette) == 0
|
||||
|
||||
|
||||
def test_cassette_load_invalid_encoding(tmpdir):
|
||||
a_file = tmpdir.join("invalid_encoding.yml")
|
||||
with open(a_file, "wb") as fd:
|
||||
fd.write(b"\xda")
|
||||
a_cassette = Cassette.load(path=str(a_file))
|
||||
assert len(a_cassette) == 0
|
||||
|
||||
|
||||
def test_cassette_not_played():
|
||||
a = Cassette("test")
|
||||
assert not a.play_count
|
||||
@@ -96,7 +113,7 @@ def make_get_request():
|
||||
@mock.patch("vcr.stubs.VCRHTTPResponse")
|
||||
def test_function_decorated_with_use_cassette_can_be_invoked_multiple_times(*args):
|
||||
decorated_function = Cassette.use(path="test")(make_get_request)
|
||||
for i in range(4):
|
||||
for _ in range(4):
|
||||
decorated_function()
|
||||
|
||||
|
||||
@@ -137,6 +154,31 @@ def test_cassette_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 _ 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)
|
||||
def test_cassette_rewound():
|
||||
a = Cassette("test")
|
||||
@@ -179,11 +221,11 @@ def test_nesting_cassette_context_managers(*args):
|
||||
with contextlib.ExitStack() as exit_stack:
|
||||
first_cassette = exit_stack.enter_context(Cassette.use(path="test"))
|
||||
exit_stack.enter_context(
|
||||
mock.patch.object(first_cassette, "play_response", return_value=first_response)
|
||||
mock.patch.object(first_cassette, "play_response", return_value=first_response),
|
||||
)
|
||||
assert_get_response_body_is("first_response")
|
||||
|
||||
# Make sure a second cassette can supercede the first
|
||||
# Make sure a second cassette can supersede the first
|
||||
with Cassette.use(path="test") as second_cassette:
|
||||
with mock.patch.object(second_cassette, "play_response", return_value=second_response):
|
||||
assert_get_response_body_is("second_response")
|
||||
@@ -208,7 +250,7 @@ def test_nesting_context_managers_by_checking_references_of_http_connection():
|
||||
|
||||
|
||||
def test_custom_patchers():
|
||||
class Test(object):
|
||||
class Test:
|
||||
attribute = None
|
||||
|
||||
with Cassette.use(path="custom_patches", custom_patches=((Test, "attribute", VCRHTTPSConnection),)):
|
||||
@@ -285,16 +327,16 @@ def test_func_path_generator():
|
||||
|
||||
|
||||
def test_use_as_decorator_on_coroutine():
|
||||
original_http_connetion = httplib.HTTPConnection
|
||||
original_http_connection = httplib.HTTPConnection
|
||||
|
||||
@Cassette.use(inject=True)
|
||||
def test_function(cassette):
|
||||
assert httplib.HTTPConnection.cassette is cassette
|
||||
assert httplib.HTTPConnection is not original_http_connetion
|
||||
assert httplib.HTTPConnection is not original_http_connection
|
||||
value = yield 1
|
||||
assert value == 1
|
||||
assert httplib.HTTPConnection.cassette is cassette
|
||||
assert httplib.HTTPConnection is not original_http_connetion
|
||||
assert httplib.HTTPConnection is not original_http_connection
|
||||
value = yield 2
|
||||
assert value == 2
|
||||
|
||||
@@ -308,15 +350,15 @@ def test_use_as_decorator_on_coroutine():
|
||||
|
||||
|
||||
def test_use_as_decorator_on_generator():
|
||||
original_http_connetion = httplib.HTTPConnection
|
||||
original_http_connection = httplib.HTTPConnection
|
||||
|
||||
@Cassette.use(inject=True)
|
||||
def test_function(cassette):
|
||||
assert httplib.HTTPConnection.cassette is cassette
|
||||
assert httplib.HTTPConnection is not original_http_connetion
|
||||
assert httplib.HTTPConnection is not original_http_connection
|
||||
yield 1
|
||||
assert httplib.HTTPConnection.cassette is cassette
|
||||
assert httplib.HTTPConnection is not original_http_connetion
|
||||
assert httplib.HTTPConnection is not original_http_connection
|
||||
yield 2
|
||||
|
||||
assert list(test_function()) == [1, 2]
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from unittest import mock
|
||||
|
||||
import pytest
|
||||
|
||||
from vcr.compat import mock
|
||||
from vcr import errors
|
||||
from vcr.cassette import Cassette
|
||||
|
||||
@@ -54,15 +55,18 @@ from vcr.cassette import Cassette
|
||||
],
|
||||
)
|
||||
def test_CannotOverwriteExistingCassetteException_get_message(
|
||||
mock_find_requests_with_most_matches, most_matches, expected_message
|
||||
mock_find_requests_with_most_matches,
|
||||
most_matches,
|
||||
expected_message,
|
||||
):
|
||||
mock_find_requests_with_most_matches.return_value = most_matches
|
||||
cassette = Cassette("path")
|
||||
failed_request = "request"
|
||||
exception_message = errors.CannotOverwriteExistingCassetteException._get_message(cassette, "request")
|
||||
expected = (
|
||||
"Can't overwrite existing cassette (%r) in your current record mode (%r).\n"
|
||||
"No match for the request (%r) was found.\n"
|
||||
"%s" % (cassette._path, cassette.record_mode, failed_request, expected_message)
|
||||
f"Can't overwrite existing cassette ({cassette._path!r}) "
|
||||
f"in your current record mode ({cassette.record_mode!r}).\n"
|
||||
f"No match for the request ({failed_request!r}) was found.\n"
|
||||
f"{expected_message}"
|
||||
)
|
||||
assert exception_message == expected
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
from six import BytesIO
|
||||
from vcr.filters import (
|
||||
remove_headers,
|
||||
replace_headers,
|
||||
remove_query_parameters,
|
||||
replace_query_parameters,
|
||||
remove_post_data_parameters,
|
||||
replace_post_data_parameters,
|
||||
decode_response,
|
||||
)
|
||||
from vcr.compat import mock
|
||||
from vcr.request import Request
|
||||
import gzip
|
||||
import json
|
||||
import zlib
|
||||
from io import BytesIO
|
||||
from unittest import mock
|
||||
|
||||
from vcr.filters import (
|
||||
decode_response,
|
||||
remove_headers,
|
||||
remove_post_data_parameters,
|
||||
remove_query_parameters,
|
||||
replace_headers,
|
||||
replace_post_data_parameters,
|
||||
replace_query_parameters,
|
||||
)
|
||||
from vcr.request import Request
|
||||
|
||||
|
||||
def test_replace_headers():
|
||||
@@ -196,7 +197,7 @@ def test_replace_json_post_data_parameters():
|
||||
("six", "doesntexist"),
|
||||
],
|
||||
)
|
||||
request_data = json.loads(request.body.decode("utf-8"))
|
||||
request_data = json.loads(request.body)
|
||||
expected_data = json.loads('{"one": "keep", "three": "tada", "four": "SHOUT"}')
|
||||
assert request_data == expected_data
|
||||
|
||||
@@ -207,8 +208,8 @@ def test_remove_json_post_data_parameters():
|
||||
request = Request("POST", "http://google.com", body, {})
|
||||
request.headers["Content-Type"] = "application/json"
|
||||
remove_post_data_parameters(request, ["id"])
|
||||
request_body_json = json.loads(request.body.decode("utf-8"))
|
||||
expected_json = json.loads(b'{"foo": "bar", "baz": "qux"}'.decode("utf-8"))
|
||||
request_body_json = json.loads(request.body)
|
||||
expected_json = json.loads(b'{"foo": "bar", "baz": "qux"}')
|
||||
assert request_body_json == expected_json
|
||||
|
||||
|
||||
@@ -220,6 +221,49 @@ def test_remove_all_json_post_data_parameters():
|
||||
assert request.body == b"{}"
|
||||
|
||||
|
||||
def test_replace_dict_post_data_parameters():
|
||||
# This tests all of:
|
||||
# 1. keeping a parameter
|
||||
# 2. removing a parameter
|
||||
# 3. replacing a parameter
|
||||
# 4. replacing a parameter using a callable
|
||||
# 5. removing a parameter using a callable
|
||||
# 6. replacing a parameter that doesn't exist
|
||||
body = {"one": "keep", "two": "lose", "three": "change", "four": "shout", "five": "whisper"}
|
||||
request = Request("POST", "http://google.com", body, {})
|
||||
request.headers["Content-Type"] = "application/x-www-form-urlencoded"
|
||||
replace_post_data_parameters(
|
||||
request,
|
||||
[
|
||||
("two", None),
|
||||
("three", "tada"),
|
||||
("four", lambda key, value, request: value.upper()),
|
||||
("five", lambda key, value, request: None),
|
||||
("six", "doesntexist"),
|
||||
],
|
||||
)
|
||||
expected_data = {"one": "keep", "three": "tada", "four": "SHOUT"}
|
||||
assert request.body == expected_data
|
||||
|
||||
|
||||
def test_remove_dict_post_data_parameters():
|
||||
# Test the backward-compatible API wrapper.
|
||||
body = {"id": "secret", "foo": "bar", "baz": "qux"}
|
||||
request = Request("POST", "http://google.com", body, {})
|
||||
request.headers["Content-Type"] = "application/x-www-form-urlencoded"
|
||||
remove_post_data_parameters(request, ["id"])
|
||||
expected_data = {"foo": "bar", "baz": "qux"}
|
||||
assert request.body == expected_data
|
||||
|
||||
|
||||
def test_remove_all_dict_post_data_parameters():
|
||||
body = {"id": "secret", "foo": "bar"}
|
||||
request = Request("POST", "http://google.com", body, {})
|
||||
request.headers["Content-Type"] = "application/x-www-form-urlencoded"
|
||||
replace_post_data_parameters(request, [("id", None), ("foo", None)])
|
||||
assert request.body == {}
|
||||
|
||||
|
||||
def test_decode_response_uncompressed():
|
||||
recorded_response = {
|
||||
"status": {"message": "OK", "code": 200},
|
||||
@@ -254,6 +298,18 @@ def test_decode_response_deflate():
|
||||
assert decoded_response["headers"]["content-length"] == [str(len(body))]
|
||||
|
||||
|
||||
def test_decode_response_deflate_already_decompressed():
|
||||
body = b"deflate message"
|
||||
gzip_response = {
|
||||
"body": {"string": body},
|
||||
"headers": {
|
||||
"content-encoding": ["deflate"],
|
||||
},
|
||||
}
|
||||
decoded_response = decode_response(gzip_response)
|
||||
assert decoded_response["body"]["string"] == body
|
||||
|
||||
|
||||
def test_decode_response_gzip():
|
||||
body = b"gzip message"
|
||||
|
||||
@@ -281,3 +337,15 @@ def test_decode_response_gzip():
|
||||
decoded_response = decode_response(gzip_response)
|
||||
assert decoded_response["body"]["string"] == body
|
||||
assert decoded_response["headers"]["content-length"] == [str(len(body))]
|
||||
|
||||
|
||||
def test_decode_response_gzip_already_decompressed():
|
||||
body = b"gzip message"
|
||||
gzip_response = {
|
||||
"body": {"string": body},
|
||||
"headers": {
|
||||
"content-encoding": ["gzip"],
|
||||
},
|
||||
}
|
||||
decoded_response = decode_response(gzip_response)
|
||||
assert decoded_response["body"]["string"] == body
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import pytest
|
||||
from vcr.serializers.jsonserializer import serialize
|
||||
|
||||
from vcr.request import Request
|
||||
from vcr.serializers.jsonserializer import serialize
|
||||
|
||||
|
||||
def test_serialize_binary():
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import itertools
|
||||
from vcr.compat import mock
|
||||
from unittest import mock
|
||||
|
||||
import pytest
|
||||
|
||||
from vcr import matchers
|
||||
from vcr import request
|
||||
from vcr import matchers, request
|
||||
|
||||
# the dict contains requests with corresponding to its key difference
|
||||
# with 'base' request.
|
||||
@@ -64,6 +63,9 @@ boto3_bytes_headers = {
|
||||
"Expect": b"100-continue",
|
||||
"Content-Length": "21",
|
||||
}
|
||||
chunked_headers = {
|
||||
"Transfer-Encoding": "chunked",
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@@ -75,10 +77,16 @@ boto3_bytes_headers = {
|
||||
),
|
||||
(
|
||||
request.Request(
|
||||
"POST", "http://host.com/", "a=1&b=2", {"Content-Type": "application/x-www-form-urlencoded"}
|
||||
"POST",
|
||||
"http://host.com/",
|
||||
"a=1&b=2",
|
||||
{"Content-Type": "application/x-www-form-urlencoded"},
|
||||
),
|
||||
request.Request(
|
||||
"POST", "http://host.com/", "b=2&a=1", {"Content-Type": "application/x-www-form-urlencoded"}
|
||||
"POST",
|
||||
"http://host.com/",
|
||||
"b=2&a=1",
|
||||
{"Content-Type": "application/x-www-form-urlencoded"},
|
||||
),
|
||||
),
|
||||
(
|
||||
@@ -87,23 +95,38 @@ boto3_bytes_headers = {
|
||||
),
|
||||
(
|
||||
request.Request(
|
||||
"POST", "http://host.com/", "a=1&b=2", {"Content-Type": "application/x-www-form-urlencoded"}
|
||||
"POST",
|
||||
"http://host.com/",
|
||||
"a=1&b=2",
|
||||
{"Content-Type": "application/x-www-form-urlencoded"},
|
||||
),
|
||||
request.Request(
|
||||
"POST", "http://host.com/", "b=2&a=1", {"Content-Type": "application/x-www-form-urlencoded"}
|
||||
"POST",
|
||||
"http://host.com/",
|
||||
"b=2&a=1",
|
||||
{"Content-Type": "application/x-www-form-urlencoded"},
|
||||
),
|
||||
),
|
||||
(
|
||||
request.Request(
|
||||
"POST", "http://host.com/", '{"a": 1, "b": 2}', {"Content-Type": "application/json"}
|
||||
"POST",
|
||||
"http://host.com/",
|
||||
'{"a": 1, "b": 2}',
|
||||
{"Content-Type": "application/json"},
|
||||
),
|
||||
request.Request(
|
||||
"POST", "http://host.com/", '{"b": 2, "a": 1}', {"content-type": "application/json"}
|
||||
"POST",
|
||||
"http://host.com/",
|
||||
'{"b": 2, "a": 1}',
|
||||
{"content-type": "application/json"},
|
||||
),
|
||||
),
|
||||
(
|
||||
request.Request(
|
||||
"POST", "http://host.com/", req1_body, {"User-Agent": "xmlrpclib", "Content-Type": "text/xml"}
|
||||
"POST",
|
||||
"http://host.com/",
|
||||
req1_body,
|
||||
{"User-Agent": "xmlrpclib", "Content-Type": "text/xml"},
|
||||
),
|
||||
request.Request(
|
||||
"POST",
|
||||
@@ -114,10 +137,16 @@ boto3_bytes_headers = {
|
||||
),
|
||||
(
|
||||
request.Request(
|
||||
"POST", "http://host.com/", '{"a": 1, "b": 2}', {"Content-Type": "application/json"}
|
||||
"POST",
|
||||
"http://host.com/",
|
||||
'{"a": 1, "b": 2}',
|
||||
{"Content-Type": "application/json"},
|
||||
),
|
||||
request.Request(
|
||||
"POST", "http://host.com/", '{"b": 2, "a": 1}', {"content-type": "application/json"}
|
||||
"POST",
|
||||
"http://host.com/",
|
||||
'{"b": 2, "a": 1}',
|
||||
{"content-type": "application/json"},
|
||||
),
|
||||
),
|
||||
(
|
||||
@@ -125,6 +154,36 @@ boto3_bytes_headers = {
|
||||
request.Request("POST", "http://aws.custom.com/", b"123", boto3_bytes_headers),
|
||||
request.Request("POST", "http://aws.custom.com/", b"123", boto3_bytes_headers),
|
||||
),
|
||||
(
|
||||
# chunked transfer encoding: decoded bytes versus encoded bytes
|
||||
request.Request("POST", "scheme1://host1.test/", b"123456789_123456", chunked_headers),
|
||||
request.Request(
|
||||
"GET",
|
||||
"scheme2://host2.test/",
|
||||
b"10\r\n123456789_123456\r\n0\r\n\r\n",
|
||||
chunked_headers,
|
||||
),
|
||||
),
|
||||
(
|
||||
# chunked transfer encoding: bytes iterator versus string iterator
|
||||
request.Request(
|
||||
"POST",
|
||||
"scheme1://host1.test/",
|
||||
iter([b"123456789_", b"123456"]),
|
||||
chunked_headers,
|
||||
),
|
||||
request.Request("GET", "scheme2://host2.test/", iter(["123456789_", "123456"]), chunked_headers),
|
||||
),
|
||||
(
|
||||
# chunked transfer encoding: bytes iterator versus single byte iterator
|
||||
request.Request(
|
||||
"POST",
|
||||
"scheme1://host1.test/",
|
||||
iter([b"123456789_", b"123456"]),
|
||||
chunked_headers,
|
||||
),
|
||||
request.Request("GET", "scheme2://host2.test/", iter(b"123456789_123456"), chunked_headers),
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_body_matcher_does_match(r1, r2):
|
||||
@@ -140,10 +199,16 @@ def test_body_matcher_does_match(r1, r2):
|
||||
),
|
||||
(
|
||||
request.Request(
|
||||
"POST", "http://host.com/", '{"a": 1, "b": 3}', {"Content-Type": "application/json"}
|
||||
"POST",
|
||||
"http://host.com/",
|
||||
'{"a": 1, "b": 3}',
|
||||
{"Content-Type": "application/json"},
|
||||
),
|
||||
request.Request(
|
||||
"POST", "http://host.com/", '{"b": 2, "a": 1}', {"content-type": "application/json"}
|
||||
"POST",
|
||||
"http://host.com/",
|
||||
'{"b": 2, "a": 1}',
|
||||
{"content-type": "application/json"},
|
||||
),
|
||||
),
|
||||
(
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import filecmp
|
||||
import json
|
||||
import shutil
|
||||
|
||||
import yaml
|
||||
|
||||
import vcr.migration
|
||||
@@ -16,9 +17,9 @@ def test_try_migrate_with_json(tmpdir):
|
||||
cassette = tmpdir.join("cassette.json").strpath
|
||||
shutil.copy("tests/fixtures/migration/old_cassette.json", cassette)
|
||||
assert vcr.migration.try_migrate(cassette)
|
||||
with open("tests/fixtures/migration/new_cassette.json", "r") as f:
|
||||
with open("tests/fixtures/migration/new_cassette.json") as f:
|
||||
expected_json = json.load(f)
|
||||
with open(cassette, "r") as f:
|
||||
with open(cassette) as f:
|
||||
actual_json = json.load(f)
|
||||
assert actual_json == expected_json
|
||||
|
||||
@@ -27,9 +28,9 @@ def test_try_migrate_with_yaml(tmpdir):
|
||||
cassette = tmpdir.join("cassette.yaml").strpath
|
||||
shutil.copy("tests/fixtures/migration/old_cassette.yaml", cassette)
|
||||
assert vcr.migration.try_migrate(cassette)
|
||||
with open("tests/fixtures/migration/new_cassette.yaml", "r") as f:
|
||||
with open("tests/fixtures/migration/new_cassette.yaml") as f:
|
||||
expected_yaml = yaml.load(f, Loader=Loader)
|
||||
with open(cassette, "r") as f:
|
||||
with open(cassette) as f:
|
||||
actual_yaml = yaml.load(f, Loader=Loader)
|
||||
assert actual_yaml == expected_yaml
|
||||
|
||||
@@ -44,4 +45,4 @@ def test_try_migrate_with_invalid_or_new_cassettes(tmpdir):
|
||||
for file_path in files:
|
||||
shutil.copy(file_path, cassette)
|
||||
assert not vcr.migration.try_migrate(cassette)
|
||||
assert filecmp.cmp(cassette, file_path) # shold not change file
|
||||
assert filecmp.cmp(cassette, file_path) # should not change file
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import pytest
|
||||
|
||||
from vcr.request import Request, HeadersDict
|
||||
from vcr.request import HeadersDict, Request
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@@ -60,7 +60,6 @@ def test_uri(method, uri):
|
||||
|
||||
|
||||
def test_HeadersDict():
|
||||
|
||||
# Simple test of CaseInsensitiveDict
|
||||
h = HeadersDict()
|
||||
assert h == {}
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
# coding: UTF-8
|
||||
import io
|
||||
import unittest
|
||||
|
||||
import six
|
||||
|
||||
from vcr.stubs import VCRHTTPResponse
|
||||
|
||||
@@ -58,7 +54,6 @@ def test_response_headers_should_have_correct_values():
|
||||
assert response.headers.get("date") == "Fri, 24 Oct 2014 18:35:37 GMT"
|
||||
|
||||
|
||||
@unittest.skipIf(six.PY2, "Regression test for Python3 only")
|
||||
def test_response_parses_correctly_and_fp_attribute_error_is_not_thrown():
|
||||
"""
|
||||
Regression test for https://github.com/kevin1024/vcrpy/issues/440
|
||||
@@ -93,11 +88,11 @@ def test_response_parses_correctly_and_fp_attribute_error_is_not_thrown():
|
||||
b"different types of cancer cells. Recently, the first HDACi was\n "
|
||||
b"approved for the "
|
||||
b"treatment of cutaneous T cell lymphomas. Most HDACi currently in\n "
|
||||
b"clinical "
|
||||
b"clinical ",
|
||||
},
|
||||
}
|
||||
vcr_response = VCRHTTPResponse(recorded_response)
|
||||
handle = io.TextIOWrapper(io.BufferedReader(vcr_response), encoding="utf-8")
|
||||
handle = io.TextIOWrapper(vcr_response, encoding="utf-8")
|
||||
handle = iter(handle)
|
||||
articles = [line for line in handle]
|
||||
articles = list(handle)
|
||||
assert len(articles) > 1
|
||||
|
||||
@@ -1,35 +1,35 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
from unittest import mock
|
||||
|
||||
import pytest
|
||||
|
||||
from vcr.compat import mock
|
||||
from vcr.request import Request
|
||||
from vcr.serialize import deserialize, serialize
|
||||
from vcr.serializers import yamlserializer, jsonserializer, compat
|
||||
from vcr.serializers import compat, jsonserializer, yamlserializer
|
||||
|
||||
|
||||
def test_deserialize_old_yaml_cassette():
|
||||
with open("tests/fixtures/migration/old_cassette.yaml", "r") as f:
|
||||
with open("tests/fixtures/migration/old_cassette.yaml") as f:
|
||||
with pytest.raises(ValueError):
|
||||
deserialize(f.read(), yamlserializer)
|
||||
|
||||
|
||||
def test_deserialize_old_json_cassette():
|
||||
with open("tests/fixtures/migration/old_cassette.json", "r") as f:
|
||||
with open("tests/fixtures/migration/old_cassette.json") as f:
|
||||
with pytest.raises(ValueError):
|
||||
deserialize(f.read(), jsonserializer)
|
||||
|
||||
|
||||
def test_deserialize_new_yaml_cassette():
|
||||
with open("tests/fixtures/migration/new_cassette.yaml", "r") as f:
|
||||
with open("tests/fixtures/migration/new_cassette.yaml") as f:
|
||||
deserialize(f.read(), yamlserializer)
|
||||
|
||||
|
||||
def test_deserialize_new_json_cassette():
|
||||
with open("tests/fixtures/migration/new_cassette.json", "r") as f:
|
||||
with open("tests/fixtures/migration/new_cassette.json") as f:
|
||||
deserialize(f.read(), jsonserializer)
|
||||
|
||||
|
||||
REQBODY_TEMPLATE = u"""\
|
||||
REQBODY_TEMPLATE = """\
|
||||
interactions:
|
||||
- request:
|
||||
body: {req_body}
|
||||
|
||||
@@ -1,17 +1,22 @@
|
||||
from vcr.stubs import VCRHTTPSConnection
|
||||
from vcr.compat import mock
|
||||
from unittest import mock
|
||||
|
||||
from pytest import mark
|
||||
|
||||
from vcr import mode
|
||||
from vcr.cassette import Cassette
|
||||
from vcr.stubs import VCRHTTPSConnection
|
||||
|
||||
|
||||
class TestVCRConnection(object):
|
||||
def test_setting_of_attributes_get_propogated_to_real_connection(self):
|
||||
class TestVCRConnection:
|
||||
def test_setting_of_attributes_get_propagated_to_real_connection(self):
|
||||
vcr_connection = VCRHTTPSConnection("www.examplehost.com")
|
||||
vcr_connection.ssl_version = "example_ssl_version"
|
||||
assert vcr_connection.real_connection.ssl_version == "example_ssl_version"
|
||||
|
||||
@mark.online
|
||||
@mock.patch("vcr.cassette.Cassette.can_play_response_for", return_value=False)
|
||||
def testing_connect(*args):
|
||||
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()
|
||||
assert vcr_connection.real_connection.sock is not None
|
||||
|
||||
199
tests/unit/test_unittest.py
Normal file
199
tests/unit/test_unittest.py
Normal file
@@ -0,0 +1,199 @@
|
||||
import os
|
||||
from unittest import TextTestRunner, defaultTestLoader
|
||||
from unittest.mock import MagicMock
|
||||
from urllib.request import urlopen
|
||||
|
||||
import pytest
|
||||
|
||||
from vcr.unittest import VCRTestCase
|
||||
|
||||
|
||||
def test_defaults():
|
||||
class MyTest(VCRTestCase):
|
||||
def test_foo(self):
|
||||
pass
|
||||
|
||||
test = run_testcase(MyTest)[0][0]
|
||||
expected_path = os.path.join(os.path.dirname(__file__), "cassettes")
|
||||
expected_name = "MyTest.test_foo.yaml"
|
||||
assert os.path.dirname(test.cassette._path) == expected_path
|
||||
assert os.path.basename(test.cassette._path) == expected_name
|
||||
|
||||
|
||||
def test_disabled():
|
||||
# Baseline vcr_enabled = True
|
||||
class MyTest(VCRTestCase):
|
||||
def test_foo(self):
|
||||
pass
|
||||
|
||||
test = run_testcase(MyTest)[0][0]
|
||||
assert hasattr(test, "cassette")
|
||||
|
||||
# Test vcr_enabled = False
|
||||
class MyTest(VCRTestCase):
|
||||
vcr_enabled = False
|
||||
|
||||
def test_foo(self):
|
||||
pass
|
||||
|
||||
test = run_testcase(MyTest)[0][0]
|
||||
assert not hasattr(test, "cassette")
|
||||
|
||||
|
||||
def test_cassette_library_dir():
|
||||
class MyTest(VCRTestCase):
|
||||
def test_foo(self):
|
||||
pass
|
||||
|
||||
def _get_cassette_library_dir(self):
|
||||
return "/testing"
|
||||
|
||||
test = run_testcase(MyTest)[0][0]
|
||||
assert test.cassette._path.startswith("/testing/")
|
||||
|
||||
|
||||
def test_cassette_name():
|
||||
class MyTest(VCRTestCase):
|
||||
def test_foo(self):
|
||||
pass
|
||||
|
||||
def _get_cassette_name(self):
|
||||
return "my-custom-name"
|
||||
|
||||
test = run_testcase(MyTest)[0][0]
|
||||
assert os.path.basename(test.cassette._path) == "my-custom-name"
|
||||
|
||||
|
||||
def test_vcr_kwargs_overridden():
|
||||
class MyTest(VCRTestCase):
|
||||
def test_foo(self):
|
||||
pass
|
||||
|
||||
def _get_vcr_kwargs(self):
|
||||
kwargs = super()._get_vcr_kwargs()
|
||||
kwargs["record_mode"] = "new_episodes"
|
||||
return kwargs
|
||||
|
||||
test = run_testcase(MyTest)[0][0]
|
||||
assert test.cassette.record_mode == "new_episodes"
|
||||
|
||||
|
||||
def test_vcr_kwargs_passed():
|
||||
class MyTest(VCRTestCase):
|
||||
def test_foo(self):
|
||||
pass
|
||||
|
||||
def _get_vcr_kwargs(self):
|
||||
return super()._get_vcr_kwargs(
|
||||
record_mode="new_episodes",
|
||||
)
|
||||
|
||||
test = run_testcase(MyTest)[0][0]
|
||||
assert test.cassette.record_mode == "new_episodes"
|
||||
|
||||
|
||||
def test_vcr_kwargs_cassette_dir():
|
||||
# Test that _get_cassette_library_dir applies if cassette_library_dir
|
||||
# is absent from vcr kwargs.
|
||||
class MyTest(VCRTestCase):
|
||||
def test_foo(self):
|
||||
pass
|
||||
|
||||
def _get_vcr_kwargs(self):
|
||||
return {
|
||||
"record_mode": "new_episodes",
|
||||
}
|
||||
|
||||
_get_cassette_library_dir = MagicMock(return_value="/testing")
|
||||
|
||||
test = run_testcase(MyTest)[0][0]
|
||||
assert test.cassette._path.startswith("/testing/")
|
||||
assert test._get_cassette_library_dir.call_count == 1
|
||||
|
||||
# Test that _get_cassette_library_dir is ignored if cassette_library_dir
|
||||
# is present in vcr kwargs.
|
||||
class MyTest(VCRTestCase):
|
||||
def test_foo(self):
|
||||
pass
|
||||
|
||||
def _get_vcr_kwargs(self):
|
||||
return {
|
||||
"cassette_library_dir": "/testing",
|
||||
}
|
||||
|
||||
_get_cassette_library_dir = MagicMock(return_value="/ignored")
|
||||
|
||||
test = run_testcase(MyTest)[0][0]
|
||||
assert test.cassette._path.startswith("/testing/")
|
||||
assert test._get_cassette_library_dir.call_count == 0
|
||||
|
||||
|
||||
@pytest.mark.online
|
||||
def test_get_vcr_with_matcher(tmpdir):
|
||||
cassette_dir = tmpdir.mkdir("cassettes")
|
||||
assert len(cassette_dir.listdir()) == 0
|
||||
|
||||
mock_matcher = MagicMock(return_value=True, __name__="MockMatcher")
|
||||
|
||||
class MyTest(VCRTestCase):
|
||||
def test_foo(self):
|
||||
self.response = urlopen("http://example.com").read()
|
||||
|
||||
def _get_vcr(self):
|
||||
myvcr = super()._get_vcr()
|
||||
myvcr.register_matcher("mymatcher", mock_matcher)
|
||||
myvcr.match_on = ["mymatcher"]
|
||||
return myvcr
|
||||
|
||||
def _get_cassette_library_dir(self):
|
||||
return str(cassette_dir)
|
||||
|
||||
# First run to fill cassette.
|
||||
test = run_testcase(MyTest)[0][0]
|
||||
assert len(test.cassette.requests) == 1
|
||||
assert not mock_matcher.called # nothing in cassette
|
||||
|
||||
# Second run to call matcher.
|
||||
test = run_testcase(MyTest)[0][0]
|
||||
assert len(test.cassette.requests) == 1
|
||||
assert mock_matcher.called
|
||||
assert (
|
||||
repr(mock_matcher.mock_calls[0])
|
||||
== "call(<Request (GET) http://example.com>, <Request (GET) http://example.com>)"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.online
|
||||
def test_testcase_playback(tmpdir):
|
||||
cassette_dir = tmpdir.mkdir("cassettes")
|
||||
assert len(cassette_dir.listdir()) == 0
|
||||
|
||||
# First test actually reads from the web.
|
||||
|
||||
class MyTest(VCRTestCase):
|
||||
def test_foo(self):
|
||||
self.response = urlopen("http://example.com").read()
|
||||
|
||||
def _get_cassette_library_dir(self):
|
||||
return str(cassette_dir)
|
||||
|
||||
test = run_testcase(MyTest)[0][0]
|
||||
assert b"illustrative examples" in test.response
|
||||
assert len(test.cassette.requests) == 1
|
||||
assert test.cassette.play_count == 0
|
||||
|
||||
# Second test reads from cassette.
|
||||
|
||||
test2 = run_testcase(MyTest)[0][0]
|
||||
assert test.cassette is not test2.cassette
|
||||
assert b"illustrative examples" in test.response
|
||||
assert len(test2.cassette.requests) == 1
|
||||
assert test2.cassette.play_count == 1
|
||||
|
||||
|
||||
def run_testcase(testcase_class):
|
||||
"""Run all the tests in a TestCase and return them."""
|
||||
suite = defaultTestLoader.loadTestsFromTestCase(testcase_class)
|
||||
tests = list(suite._tests)
|
||||
result = TextTestRunner().run(suite)
|
||||
return tests, result
|
||||
@@ -1,20 +1,22 @@
|
||||
import http.client as httplib
|
||||
import os
|
||||
from pathlib import Path
|
||||
from unittest import mock
|
||||
|
||||
import pytest
|
||||
from six.moves import http_client as httplib
|
||||
|
||||
from vcr import VCR, use_cassette
|
||||
from vcr.compat import mock
|
||||
from vcr import VCR, mode, use_cassette
|
||||
from vcr.patch import _HTTPConnection, force_reset
|
||||
from vcr.request import Request
|
||||
from vcr.stubs import VCRHTTPSConnection
|
||||
from vcr.patch import _HTTPConnection, force_reset
|
||||
|
||||
|
||||
def test_vcr_use_cassette():
|
||||
record_mode = mock.Mock()
|
||||
test_vcr = VCR(record_mode=record_mode)
|
||||
with mock.patch(
|
||||
"vcr.cassette.Cassette.load", return_value=mock.MagicMock(inject=False)
|
||||
"vcr.cassette.Cassette.load",
|
||||
return_value=mock.MagicMock(inject=False),
|
||||
) as mock_cassette_load:
|
||||
|
||||
@test_vcr.use_cassette("test")
|
||||
@@ -31,7 +33,7 @@ def test_vcr_use_cassette():
|
||||
function()
|
||||
assert mock_cassette_load.call_args[1]["record_mode"] == test_vcr.record_mode
|
||||
|
||||
# Ensure that explicitly provided arguments still supercede
|
||||
# Ensure that explicitly provided arguments still supersede
|
||||
# those on the vcr.
|
||||
new_record_mode = mock.Mock()
|
||||
|
||||
@@ -40,7 +42,7 @@ def test_vcr_use_cassette():
|
||||
|
||||
|
||||
def test_vcr_before_record_request_params():
|
||||
base_path = "http://httpbin.org/"
|
||||
base_path = "http://whatever.test/"
|
||||
|
||||
def before_record_cb(request):
|
||||
if request.path != "/get":
|
||||
@@ -70,16 +72,19 @@ def test_vcr_before_record_request_params():
|
||||
|
||||
# Test filter_headers
|
||||
request = Request(
|
||||
"GET", base_path + "?foo=bar", "", {"cookie": "test", "other": "fun", "bert": "nobody"}
|
||||
"GET",
|
||||
base_path + "?foo=bar",
|
||||
"",
|
||||
{"cookie": "test", "other": "fun", "bert": "nobody"},
|
||||
)
|
||||
assert cassette.filter_request(request).headers == {"other": "fun", "bert": "ernie"}
|
||||
|
||||
# Test ignore_hosts
|
||||
request = Request("GET", "http://www.test.com" + "?foo=bar", "", {"cookie": "test", "other": "fun"})
|
||||
request = Request("GET", "http://www.test.com?foo=bar", "", {"cookie": "test", "other": "fun"})
|
||||
assert cassette.filter_request(request) is None
|
||||
|
||||
# Test ignore_localhost
|
||||
request = Request("GET", "http://localhost:8000" + "?foo=bar", "", {"cookie": "test", "other": "fun"})
|
||||
request = Request("GET", "http://localhost:8000?foo=bar", "", {"cookie": "test", "other": "fun"})
|
||||
assert cassette.filter_request(request) is None
|
||||
|
||||
with test_vcr.use_cassette("test", before_record_request=None) as cassette:
|
||||
@@ -95,7 +100,6 @@ def test_vcr_before_record_response_iterable():
|
||||
|
||||
# Prevent actually saving the cassette
|
||||
with mock.patch("vcr.cassette.FilesystemPersister.save_cassette"):
|
||||
|
||||
# Baseline: non-iterable before_record_response should work
|
||||
mock_filter = mock.Mock()
|
||||
vcr = VCR(before_record_response=mock_filter)
|
||||
@@ -119,7 +123,6 @@ def test_before_record_response_as_filter():
|
||||
|
||||
# Prevent actually saving the cassette
|
||||
with mock.patch("vcr.cassette.FilesystemPersister.save_cassette"):
|
||||
|
||||
filter_all = mock.Mock(return_value=None)
|
||||
vcr = VCR(before_record_response=filter_all)
|
||||
with vcr.use_cassette("test") as cassette:
|
||||
@@ -133,7 +136,6 @@ def test_vcr_path_transformer():
|
||||
|
||||
# Prevent actually saving the cassette
|
||||
with mock.patch("vcr.cassette.FilesystemPersister.save_cassette"):
|
||||
|
||||
# Baseline: path should be unchanged
|
||||
vcr = VCR()
|
||||
with vcr.use_cassette("test") as cassette:
|
||||
@@ -170,7 +172,7 @@ def test_fixtures_with_use_cassette(random_fixture):
|
||||
|
||||
|
||||
def test_custom_patchers():
|
||||
class Test(object):
|
||||
class Test:
|
||||
attribute = None
|
||||
attribute2 = None
|
||||
|
||||
@@ -188,11 +190,11 @@ def test_custom_patchers():
|
||||
def test_inject_cassette():
|
||||
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):
|
||||
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():
|
||||
pass
|
||||
|
||||
@@ -201,7 +203,7 @@ def test_inject_cassette():
|
||||
|
||||
|
||||
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)
|
||||
def changing_defaults(cassette, checks):
|
||||
@@ -212,10 +214,10 @@ def test_with_current_defaults():
|
||||
checks(cassette)
|
||||
|
||||
def assert_record_mode_once(cassette):
|
||||
assert cassette.record_mode == "once"
|
||||
assert cassette.record_mode == mode.ONCE
|
||||
|
||||
def assert_record_mode_all(cassette):
|
||||
assert cassette.record_mode == "all"
|
||||
assert cassette.record_mode == mode.ALL
|
||||
|
||||
changing_defaults(assert_record_mode_once)
|
||||
current_defaults(assert_record_mode_once)
|
||||
@@ -226,7 +228,7 @@ def test_with_current_defaults():
|
||||
|
||||
|
||||
def test_cassette_library_dir_with_decoration_and_no_explicit_path():
|
||||
library_dir = "/libary_dir"
|
||||
library_dir = "/library_dir"
|
||||
vcr = VCR(inject_cassette=True, cassette_library_dir=library_dir)
|
||||
|
||||
@vcr.use_cassette()
|
||||
@@ -237,7 +239,7 @@ def test_cassette_library_dir_with_decoration_and_no_explicit_path():
|
||||
|
||||
|
||||
def test_cassette_library_dir_with_decoration_and_explicit_path():
|
||||
library_dir = "/libary_dir"
|
||||
library_dir = "/library_dir"
|
||||
vcr = VCR(inject_cassette=True, cassette_library_dir=library_dir)
|
||||
|
||||
@vcr.use_cassette(path="custom_name")
|
||||
@@ -248,7 +250,7 @@ def test_cassette_library_dir_with_decoration_and_explicit_path():
|
||||
|
||||
|
||||
def test_cassette_library_dir_with_decoration_and_super_explicit_path():
|
||||
library_dir = "/libary_dir"
|
||||
library_dir = "/library_dir"
|
||||
vcr = VCR(inject_cassette=True, cassette_library_dir=library_dir)
|
||||
|
||||
@vcr.use_cassette(path=os.path.join(library_dir, "custom_name"))
|
||||
@@ -259,9 +261,11 @@ def test_cassette_library_dir_with_decoration_and_super_explicit_path():
|
||||
|
||||
|
||||
def test_cassette_library_dir_with_path_transformer():
|
||||
library_dir = "/libary_dir"
|
||||
library_dir = "/library_dir"
|
||||
vcr = VCR(
|
||||
inject_cassette=True, cassette_library_dir=library_dir, path_transformer=lambda path: path + ".json"
|
||||
inject_cassette=True,
|
||||
cassette_library_dir=library_dir,
|
||||
path_transformer=lambda path: path + ".json",
|
||||
)
|
||||
|
||||
@vcr.use_cassette()
|
||||
@@ -360,3 +364,11 @@ def test_dynamically_added(self):
|
||||
|
||||
TestVCRClass.test_dynamically_added = test_dynamically_added
|
||||
del test_dynamically_added
|
||||
|
||||
|
||||
def test_path_class_as_cassette():
|
||||
path = Path(__file__).parent.parent.joinpath(
|
||||
"integration/cassettes/test_httpx_test_test_behind_proxy.yml",
|
||||
)
|
||||
with use_cassette(path):
|
||||
pass
|
||||
|
||||
@@ -2,15 +2,10 @@ import sys
|
||||
|
||||
|
||||
def test_vcr_import_deprecation(recwarn):
|
||||
|
||||
if "vcr" in sys.modules:
|
||||
# Remove imported module entry if already loaded in another test
|
||||
del sys.modules["vcr"]
|
||||
|
||||
import vcr # noqa: F401
|
||||
|
||||
if sys.version_info[0] == 2:
|
||||
assert len(recwarn) == 1
|
||||
assert issubclass(recwarn[0].category, DeprecationWarning)
|
||||
else:
|
||||
assert len(recwarn) == 0
|
||||
assert len(recwarn) == 0
|
||||
|
||||
57
tox.ini
57
tox.ini
@@ -1,13 +1,23 @@
|
||||
[tox]
|
||||
skip_missing_interpreters=true
|
||||
envlist =
|
||||
envlist =
|
||||
cov-clean,
|
||||
lint,
|
||||
{py27,py35,py36,py37,py38,pypy,pypy3}-{requests,httplib2,urllib3,tornado4,boto3},
|
||||
{py35,py36,py37,py38}-{aiohttp},
|
||||
{py38,py39,py310,py311}-{requests-urllib3-1,httplib2,urllib3-1,tornado4,boto3,aiohttp,httpx},
|
||||
{py310,py311}-{requests-urllib3-2,urllib3-2},
|
||||
{pypy3}-{requests-urllib3-1,httplib2,urllib3-1,tornado4,boto3},
|
||||
{py310}-httpx019,
|
||||
cov-report
|
||||
|
||||
|
||||
[gh-actions]
|
||||
python =
|
||||
3.8: py38
|
||||
3.9: py39
|
||||
3.10: py310, lint
|
||||
3.11: py311
|
||||
pypy-3: pypy3
|
||||
|
||||
# Coverage environment tasks: cov-clean and cov-report
|
||||
# https://pytest-cov.readthedocs.io/en/latest/tox.html
|
||||
[testenv:cov-clean]
|
||||
@@ -18,7 +28,7 @@ commands = coverage erase
|
||||
[testenv:cov-report]
|
||||
deps = coverage
|
||||
skip_install=true
|
||||
commands =
|
||||
commands =
|
||||
coverage html
|
||||
coverage report --fail-under=90
|
||||
|
||||
@@ -27,12 +37,12 @@ skipsdist = True
|
||||
commands =
|
||||
black --version
|
||||
black --check --diff .
|
||||
flake8 --version
|
||||
flake8 --exclude=./docs/conf.py,./.tox/
|
||||
pyflakes ./docs/conf.py
|
||||
deps =
|
||||
flake8
|
||||
ruff --version
|
||||
ruff check .
|
||||
deps =
|
||||
black
|
||||
ruff
|
||||
basepython = python3.10
|
||||
|
||||
[testenv]
|
||||
# Need to use develop install so that paths
|
||||
@@ -40,32 +50,33 @@ deps =
|
||||
usedevelop=true
|
||||
commands =
|
||||
./runtests.sh --cov=./vcr --cov-branch --cov-report=xml --cov-append {posargs}
|
||||
allowlist_externals =
|
||||
./runtests.sh
|
||||
deps =
|
||||
Flask
|
||||
mock
|
||||
Werkzeug==2.0.3
|
||||
pytest
|
||||
pytest-httpbin
|
||||
pytest-httpbin>=1.0.1
|
||||
pytest-cov
|
||||
PyYAML
|
||||
ipaddress
|
||||
requests: requests>=2.22.0
|
||||
httplib2: httplib2
|
||||
urllib3: urllib3
|
||||
{py27,py35,py36,pypy}-tornado4: tornado>=4,<5
|
||||
{py27,py35,py36,pypy}-tornado4: pytest-tornado
|
||||
{py27,py35,py36}-tornado4: pycurl
|
||||
urllib3-1: urllib3<2
|
||||
urllib3-2: urllib3<3
|
||||
boto3: boto3
|
||||
boto3: urllib3
|
||||
aiohttp: aiohttp
|
||||
aiohttp: pytest-asyncio
|
||||
aiohttp: pytest-aiohttp
|
||||
depends =
|
||||
{py27,py35,py36,py37,pypy}-{lint,requests,httplib2,urllib3,tornado4,boto3},{py35,py36,py37}-{aiohttp}: cov-clean
|
||||
cov-report: {py27,py35,py36,py37,pypy}-{lint,requests,httplib2,urllib3,tornado4,boto3},{py35,py36,py37}-{aiohttp}
|
||||
httpx: httpx
|
||||
{py38,py39,py310}-{httpx}: httpx
|
||||
{py38,py39,py310}-{httpx}: pytest-asyncio
|
||||
httpx: httpx>0.19
|
||||
httpx019: httpx==0.19
|
||||
{py38,py39,py310}-{httpx}: pytest-asyncio
|
||||
depends =
|
||||
lint,{py38,py39,py310,py311,pypy3}-{requests-urllib3-1,httplib2,urllib3-1,tornado4,boto3},{py310,py311}-{requests-urllib3-2,urllib3-2},{py38,py39,py310,py311}-{aiohttp},{py38,py39,py310,py311}-{httpx}: cov-clean
|
||||
cov-report: lint,{py38,py39,py310,py311,pypy3}-{requests-urllib3-1,httplib2,urllib3-1,tornado4,boto3},{py310,py311}-{requests-urllib3-2,urllib3-2},{py38,py39,py310,py311}-{aiohttp}
|
||||
passenv =
|
||||
AWS_ACCESS_KEY_ID
|
||||
AWS_DEFAULT_REGION
|
||||
AWS_SECRET_ACCESS_KEY
|
||||
|
||||
[flake8]
|
||||
max_line_length = 110
|
||||
|
||||
BIN
vcr.png
BIN
vcr.png
Binary file not shown.
|
Before Width: | Height: | Size: 240 KiB After Width: | Height: | Size: 114 KiB |
@@ -1,23 +1,10 @@
|
||||
import logging
|
||||
import warnings
|
||||
import sys
|
||||
from logging import NullHandler
|
||||
|
||||
from .config import VCR
|
||||
from .record_mode import RecordMode as mode # noqa: F401
|
||||
|
||||
# Set default logging handler to avoid "No handler found" warnings.
|
||||
try: # Python 2.7+
|
||||
from logging import NullHandler
|
||||
except ImportError:
|
||||
|
||||
class NullHandler(logging.Handler):
|
||||
def emit(self, record):
|
||||
pass
|
||||
|
||||
|
||||
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,
|
||||
)
|
||||
__version__ = "5.1.0"
|
||||
|
||||
logging.getLogger(__name__).addHandler(NullHandler())
|
||||
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
async def handle_coroutine(vcr, fn): # noqa: E999
|
||||
async def handle_coroutine(vcr, fn):
|
||||
with vcr as cassette:
|
||||
return await fn(cassette) # noqa: E999
|
||||
return await fn(cassette)
|
||||
|
||||
@@ -1,39 +1,26 @@
|
||||
import collections
|
||||
import contextlib
|
||||
import copy
|
||||
import sys
|
||||
import inspect
|
||||
import logging
|
||||
import sys
|
||||
from asyncio import iscoroutinefunction
|
||||
|
||||
import wrapt
|
||||
|
||||
from .compat import contextlib
|
||||
from ._handle_coroutine import handle_coroutine
|
||||
from .errors import UnhandledHTTPRequestError
|
||||
from .matchers import requests_match, uri, method, get_matchers_results
|
||||
from .matchers import get_matchers_results, method, requests_match, uri
|
||||
from .patch import CassettePatcherBuilder
|
||||
from .persisters.filesystem import CassetteDecodeError, CassetteNotFoundError, FilesystemPersister
|
||||
from .record_mode import RecordMode
|
||||
from .serializers import yamlserializer
|
||||
from .persisters.filesystem import FilesystemPersister
|
||||
from .util import partition_dict
|
||||
|
||||
try:
|
||||
from asyncio import iscoroutinefunction
|
||||
except ImportError:
|
||||
|
||||
def iscoroutinefunction(*args, **kwargs):
|
||||
return False
|
||||
|
||||
|
||||
if sys.version_info[:2] >= (3, 5):
|
||||
from ._handle_coroutine import handle_coroutine
|
||||
else:
|
||||
|
||||
def handle_coroutine(*args, **kwags):
|
||||
raise NotImplementedError("Not implemented on Python 2")
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CassetteContextDecorator(object):
|
||||
class CassetteContextDecorator:
|
||||
"""Context manager/decorator that handles installing the cassette and
|
||||
removing cassettes.
|
||||
|
||||
@@ -51,7 +38,11 @@ class CassetteContextDecorator(object):
|
||||
this class as a context manager in ``__exit__``.
|
||||
"""
|
||||
|
||||
_non_cassette_arguments = ("path_transformer", "func_path_generator")
|
||||
_non_cassette_arguments = (
|
||||
"path_transformer",
|
||||
"func_path_generator",
|
||||
"record_on_exception",
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_args(cls, cassette_class, **kwargs):
|
||||
@@ -61,6 +52,7 @@ class CassetteContextDecorator(object):
|
||||
self.cls = cls
|
||||
self._args_getter = args_getter
|
||||
self.__finish = None
|
||||
self.__cassette = None
|
||||
|
||||
def _patch_generator(self, cassette):
|
||||
with contextlib.ExitStack() as exit_stack:
|
||||
@@ -70,9 +62,6 @@ class CassetteContextDecorator(object):
|
||||
log.debug(log_format.format(action="Entering", path=cassette._path))
|
||||
yield cassette
|
||||
log.debug(log_format.format(action="Exiting", path=cassette._path))
|
||||
# TODO(@IvanMalison): Hmmm. it kind of feels like this should be
|
||||
# somewhere else.
|
||||
cassette._save()
|
||||
|
||||
def __enter__(self):
|
||||
# This assertion is here to prevent the dangerous behavior
|
||||
@@ -85,15 +74,28 @@ class CassetteContextDecorator(object):
|
||||
# pass
|
||||
assert self.__finish is None, "Cassette already open."
|
||||
other_kwargs, cassette_kwargs = partition_dict(
|
||||
lambda key, _: key in self._non_cassette_arguments, self._args_getter()
|
||||
lambda key, _: key in self._non_cassette_arguments,
|
||||
self._args_getter(),
|
||||
)
|
||||
if other_kwargs.get("path_transformer"):
|
||||
transformer = other_kwargs["path_transformer"]
|
||||
cassette_kwargs["path"] = transformer(cassette_kwargs["path"])
|
||||
self.__finish = self._patch_generator(self.cls.load(**cassette_kwargs))
|
||||
self.__cassette = self.cls.load(**cassette_kwargs)
|
||||
self.__finish = self._patch_generator(self.__cassette)
|
||||
return next(self.__finish)
|
||||
|
||||
def __exit__(self, *args):
|
||||
def __exit__(self, *exc_info):
|
||||
exception_was_raised = any(exc_info)
|
||||
record_on_exception = self._args_getter().get("record_on_exception", True)
|
||||
if record_on_exception or not exception_was_raised:
|
||||
self.__cassette._save()
|
||||
self.__cassette = None
|
||||
# Fellow programmer, don't remove this `next`, if `self.__finish` is
|
||||
# not consumed the unpatcher functions accumulated in the `exit_stack`
|
||||
# object created in `_patch_generator` will not be called until
|
||||
# `exit_stack` is not garbage collected.
|
||||
# This works in CPython but not in Pypy, where the unpatchers will not
|
||||
# be called until much later.
|
||||
next(self.__finish, None)
|
||||
self.__finish = None
|
||||
|
||||
@@ -159,7 +161,7 @@ class CassetteContextDecorator(object):
|
||||
return new_args_getter
|
||||
|
||||
|
||||
class Cassette(object):
|
||||
class Cassette:
|
||||
"""A container for recorded requests and responses"""
|
||||
|
||||
@classmethod
|
||||
@@ -182,12 +184,13 @@ class Cassette(object):
|
||||
path,
|
||||
serializer=None,
|
||||
persister=None,
|
||||
record_mode="once",
|
||||
record_mode=RecordMode.ONCE,
|
||||
match_on=(uri, method),
|
||||
before_record_request=None,
|
||||
before_record_response=None,
|
||||
custom_patches=(),
|
||||
inject=False,
|
||||
allow_playback_repeats=False,
|
||||
):
|
||||
self._persister = persister or FilesystemPersister
|
||||
self._path = path
|
||||
@@ -199,6 +202,7 @@ class Cassette(object):
|
||||
self.inject = inject
|
||||
self.record_mode = record_mode
|
||||
self.custom_patches = custom_patches
|
||||
self.allow_playback_repeats = allow_playback_repeats
|
||||
|
||||
# self.data is the list of (req, resp) tuples
|
||||
self.data = []
|
||||
@@ -213,7 +217,7 @@ class Cassette(object):
|
||||
@property
|
||||
def all_played(self):
|
||||
"""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
|
||||
def requests(self):
|
||||
@@ -225,7 +229,7 @@ class Cassette(object):
|
||||
|
||||
@property
|
||||
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):
|
||||
"""Add a request, response pair to this cassette"""
|
||||
@@ -257,7 +261,7 @@ class Cassette(object):
|
||||
|
||||
def can_play_response_for(self, request):
|
||||
request = self._before_record_request(request)
|
||||
return request and request in self and self.record_mode != "all" and self.rewound
|
||||
return request and request in self and self.record_mode != RecordMode.ALL and self.rewound
|
||||
|
||||
def play_response(self, request):
|
||||
"""
|
||||
@@ -265,12 +269,12 @@ class Cassette(object):
|
||||
hasn't been played back before, and mark it as played
|
||||
"""
|
||||
for index, response in self._responses(request):
|
||||
if self.play_counts[index] == 0:
|
||||
if self.play_counts[index] == 0 or self.allow_playback_repeats:
|
||||
self.play_counts[index] += 1
|
||||
return response
|
||||
# The cassette doesn't contain the request asked for.
|
||||
raise UnhandledHTTPRequestError(
|
||||
"The cassette (%r) doesn't contain the request (%r) asked for" % (self._path, request)
|
||||
f"The cassette ({self._path!r}) doesn't contain the request ({request!r}) asked for",
|
||||
)
|
||||
|
||||
def responses_of(self, request):
|
||||
@@ -285,7 +289,7 @@ class Cassette(object):
|
||||
return responses
|
||||
# The cassette doesn't contain the request asked for.
|
||||
raise UnhandledHTTPRequestError(
|
||||
"The cassette (%r) doesn't contain the request (%r) asked for" % (self._path, request)
|
||||
f"The cassette ({self._path!r}) doesn't contain the request ({request!r}) asked for",
|
||||
)
|
||||
|
||||
def rewind(self):
|
||||
@@ -304,7 +308,7 @@ class Cassette(object):
|
||||
"""
|
||||
best_matches = []
|
||||
request = self._before_record_request(request)
|
||||
for index, (stored_request, response) in enumerate(self.data):
|
||||
for _, (stored_request, _) in enumerate(self.data):
|
||||
successes, fails = get_matchers_results(request, stored_request, self._match_on)
|
||||
best_matches.append((len(successes), stored_request, successes, fails))
|
||||
best_matches.sort(key=lambda t: t[0], reverse=True)
|
||||
@@ -342,11 +346,11 @@ class Cassette(object):
|
||||
self.append(request, response)
|
||||
self.dirty = False
|
||||
self.rewound = True
|
||||
except ValueError:
|
||||
except (CassetteDecodeError, CassetteNotFoundError):
|
||||
pass
|
||||
|
||||
def __str__(self):
|
||||
return "<Cassette containing {} recorded response(s)>".format(len(self))
|
||||
return f"<Cassette containing {len(self)} recorded response(s)>"
|
||||
|
||||
def __len__(self):
|
||||
"""Return the number of request,response pairs stored in here"""
|
||||
@@ -354,7 +358,7 @@ class Cassette(object):
|
||||
|
||||
def __contains__(self, request):
|
||||
"""Return whether or not a request has been stored"""
|
||||
for index, response in self._responses(request):
|
||||
if self.play_counts[index] == 0:
|
||||
for index, _ in self._responses(request):
|
||||
if self.play_counts[index] == 0 or self.allow_playback_repeats:
|
||||
return True
|
||||
return False
|
||||
|
||||
@@ -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"]
|
||||
@@ -1,25 +1,20 @@
|
||||
import copy
|
||||
|
||||
try:
|
||||
from collections import abc as collections_abc # only works on python 3.3+
|
||||
except ImportError:
|
||||
import collections as collections_abc
|
||||
import functools
|
||||
import inspect
|
||||
import os
|
||||
import types
|
||||
from collections import abc as collections_abc
|
||||
from pathlib import Path
|
||||
|
||||
import six
|
||||
|
||||
from . import filters, matchers
|
||||
from .cassette import Cassette
|
||||
from .serializers import yamlserializer, jsonserializer
|
||||
from .persisters.filesystem import FilesystemPersister
|
||||
from .util import compose, auto_decorate
|
||||
from . import matchers
|
||||
from . import filters
|
||||
from .record_mode import RecordMode
|
||||
from .serializers import jsonserializer, yamlserializer
|
||||
from .util import auto_decorate, compose
|
||||
|
||||
|
||||
class VCR(object):
|
||||
class VCR:
|
||||
@staticmethod
|
||||
def is_test_method(method_name, function):
|
||||
return method_name.startswith("test") and isinstance(function, types.FunctionType)
|
||||
@@ -40,7 +35,7 @@ class VCR(object):
|
||||
custom_patches=(),
|
||||
filter_query_parameters=(),
|
||||
ignore_hosts=(),
|
||||
record_mode="once",
|
||||
record_mode=RecordMode.ONCE,
|
||||
ignore_localhost=False,
|
||||
filter_headers=(),
|
||||
before_record_response=None,
|
||||
@@ -52,6 +47,7 @@ class VCR(object):
|
||||
cassette_library_dir=None,
|
||||
func_path_generator=None,
|
||||
decode_compressed_response=False,
|
||||
record_on_exception=True,
|
||||
):
|
||||
self.serializer = serializer
|
||||
self.match_on = match_on
|
||||
@@ -83,13 +79,14 @@ class VCR(object):
|
||||
self.path_transformer = path_transformer
|
||||
self.func_path_generator = func_path_generator
|
||||
self.decode_compressed_response = decode_compressed_response
|
||||
self.record_on_exception = record_on_exception
|
||||
self._custom_patches = tuple(custom_patches)
|
||||
|
||||
def _get_serializer(self, serializer_name):
|
||||
try:
|
||||
serializer = self.serializers[serializer_name]
|
||||
except KeyError:
|
||||
raise KeyError("Serializer {} doesn't exist or isn't registered".format(serializer_name))
|
||||
raise KeyError(f"Serializer {serializer_name} doesn't exist or isn't registered") from None
|
||||
return serializer
|
||||
|
||||
def _get_matchers(self, matcher_names):
|
||||
@@ -98,11 +95,11 @@ class VCR(object):
|
||||
for m in matcher_names:
|
||||
matchers.append(self.matchers[m])
|
||||
except KeyError:
|
||||
raise KeyError("Matcher {} doesn't exist or isn't registered".format(m))
|
||||
raise KeyError(f"Matcher {m} doesn't exist or isn't registered") from None
|
||||
return matchers
|
||||
|
||||
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, Path)):
|
||||
function = path
|
||||
# Assume this is an attempt to decorate a function
|
||||
return self._use_cassette(**kwargs)(function)
|
||||
@@ -126,6 +123,7 @@ class VCR(object):
|
||||
func_path_generator = kwargs.get("func_path_generator", self.func_path_generator)
|
||||
cassette_library_dir = kwargs.get("cassette_library_dir", self.cassette_library_dir)
|
||||
additional_matchers = kwargs.get("additional_matchers", ())
|
||||
record_on_exception = kwargs.get("record_on_exception", self.record_on_exception)
|
||||
|
||||
if cassette_library_dir:
|
||||
|
||||
@@ -151,6 +149,8 @@ class VCR(object):
|
||||
"inject": kwargs.get("inject_cassette", self.inject_cassette),
|
||||
"path_transformer": path_transformer,
|
||||
"func_path_generator": func_path_generator,
|
||||
"allow_playback_repeats": kwargs.get("allow_playback_repeats", False),
|
||||
"record_on_exception": record_on_exception,
|
||||
}
|
||||
path = kwargs.get("path")
|
||||
if path:
|
||||
@@ -160,7 +160,8 @@ class VCR(object):
|
||||
def _build_before_record_response(self, options):
|
||||
before_record_response = options.get("before_record_response", self.before_record_response)
|
||||
decode_compressed_response = options.get(
|
||||
"decode_compressed_response", self.decode_compressed_response
|
||||
"decode_compressed_response",
|
||||
self.decode_compressed_response,
|
||||
)
|
||||
filter_functions = []
|
||||
if decode_compressed_response:
|
||||
@@ -184,10 +185,12 @@ class VCR(object):
|
||||
filter_headers = options.get("filter_headers", self.filter_headers)
|
||||
filter_query_parameters = options.get("filter_query_parameters", self.filter_query_parameters)
|
||||
filter_post_data_parameters = options.get(
|
||||
"filter_post_data_parameters", self.filter_post_data_parameters
|
||||
"filter_post_data_parameters",
|
||||
self.filter_post_data_parameters,
|
||||
)
|
||||
before_record_request = options.get(
|
||||
"before_record_request", options.get("before_record", self.before_record_request)
|
||||
"before_record_request",
|
||||
options.get("before_record", self.before_record_request),
|
||||
)
|
||||
ignore_hosts = options.get("ignore_hosts", self.ignore_hosts)
|
||||
ignore_localhost = options.get("ignore_localhost", self.ignore_localhost)
|
||||
@@ -197,12 +200,12 @@ class VCR(object):
|
||||
if filter_query_parameters:
|
||||
replacements = [p if isinstance(p, tuple) else (p, None) for p in filter_query_parameters]
|
||||
filter_functions.append(
|
||||
functools.partial(filters.replace_query_parameters, replacements=replacements)
|
||||
functools.partial(filters.replace_query_parameters, replacements=replacements),
|
||||
)
|
||||
if filter_post_data_parameters:
|
||||
replacements = [p if isinstance(p, tuple) else (p, None) for p in filter_post_data_parameters]
|
||||
filter_functions.append(
|
||||
functools.partial(filters.replace_post_data_parameters, replacements=replacements)
|
||||
functools.partial(filters.replace_post_data_parameters, replacements=replacements),
|
||||
)
|
||||
|
||||
hosts_to_ignore = set(ignore_hosts)
|
||||
@@ -217,7 +220,7 @@ class VCR(object):
|
||||
filter_functions.extend(before_record_request)
|
||||
|
||||
def before_record_request(request):
|
||||
request = copy.copy(request)
|
||||
request = copy.deepcopy(request)
|
||||
for function in filter_functions:
|
||||
if request is None:
|
||||
break
|
||||
@@ -251,4 +254,5 @@ class VCR(object):
|
||||
|
||||
def test_case(self, predicate=None):
|
||||
predicate = predicate or self.is_test_method
|
||||
return six.with_metaclass(auto_decorate(self.use_cassette, predicate))
|
||||
metaclass = auto_decorate(self.use_cassette, predicate)
|
||||
return metaclass("temporary_class", (), {})
|
||||
|
||||
@@ -3,7 +3,7 @@ class CannotOverwriteExistingCassetteException(Exception):
|
||||
self.cassette = kwargs["cassette"]
|
||||
self.failed_request = kwargs["failed_request"]
|
||||
message = self._get_message(kwargs["cassette"], kwargs["failed_request"])
|
||||
super(CannotOverwriteExistingCassetteException, self).__init__(message)
|
||||
super().__init__(message)
|
||||
|
||||
@staticmethod
|
||||
def _get_message(cassette, failed_request):
|
||||
@@ -13,30 +13,29 @@ class CannotOverwriteExistingCassetteException(Exception):
|
||||
best_matches = cassette.find_requests_with_most_matches(failed_request)
|
||||
if best_matches:
|
||||
# Build a comprehensible message to put in the exception.
|
||||
best_matches_msg = "Found {} similar requests with {} different matcher(s) :\n".format(
|
||||
len(best_matches), len(best_matches[0][2])
|
||||
best_matches_msg = (
|
||||
f"Found {len(best_matches)} similar requests "
|
||||
f"with {len(best_matches[0][2])} different matcher(s) :\n"
|
||||
)
|
||||
|
||||
for idx, best_match in enumerate(best_matches, start=1):
|
||||
request, succeeded_matchers, failed_matchers_assertion_msgs = best_match
|
||||
best_matches_msg += (
|
||||
"\n%s - (%r).\n"
|
||||
"Matchers succeeded : %s\n"
|
||||
"Matchers failed :\n" % (idx, request, succeeded_matchers)
|
||||
f"\n{idx} - ({request!r}).\n"
|
||||
f"Matchers succeeded : {succeeded_matchers}\n"
|
||||
"Matchers failed :\n"
|
||||
)
|
||||
for failed_matcher, assertion_msg in failed_matchers_assertion_msgs:
|
||||
best_matches_msg += "%s - assertion failure :\n" "%s\n" % (failed_matcher, assertion_msg)
|
||||
best_matches_msg += f"{failed_matcher} - assertion failure :\n{assertion_msg}\n"
|
||||
else:
|
||||
best_matches_msg = "No similar requests, that have not been played, found."
|
||||
return (
|
||||
"Can't overwrite existing cassette (%r) in "
|
||||
"your current record mode (%r).\n"
|
||||
"No match for the request (%r) was found.\n"
|
||||
"%s" % (cassette._path, cassette.record_mode, failed_request, best_matches_msg)
|
||||
f"Can't overwrite existing cassette ({cassette._path!r}) in "
|
||||
f"your current record mode ({cassette.record_mode!r}).\n"
|
||||
f"No match for the request ({failed_request!r}) was found.\n"
|
||||
f"{best_matches_msg}"
|
||||
)
|
||||
|
||||
|
||||
class UnhandledHTTPRequestError(KeyError):
|
||||
"""Raised when a cassette does not contain the request we want."""
|
||||
|
||||
pass
|
||||
|
||||
@@ -1,20 +1,18 @@
|
||||
from six import BytesIO, text_type
|
||||
from six.moves.urllib.parse import urlparse, urlencode, urlunparse
|
||||
import copy
|
||||
import json
|
||||
import zlib
|
||||
from io import BytesIO
|
||||
from urllib.parse import urlencode, urlparse, urlunparse
|
||||
|
||||
from .util import CaseInsensitiveDict
|
||||
|
||||
|
||||
def replace_headers(request, replacements):
|
||||
"""
|
||||
Replace headers in request according to replacements. The replacements
|
||||
should be a list of (key, value) pairs where the value can be any of:
|
||||
1. A simple replacement string value.
|
||||
2. None to remove the given header.
|
||||
3. A callable which accepts (key, value, request) and returns a string
|
||||
value or None.
|
||||
"""Replace headers in request according to replacements.
|
||||
The replacements should be a list of (key, value) pairs where the value can be any of:
|
||||
1. A simple replacement string value.
|
||||
2. None to remove the given header.
|
||||
3. A callable which accepts (key, value, request) and returns a string value or None.
|
||||
"""
|
||||
new_headers = request.headers.copy()
|
||||
for k, rv in replacements:
|
||||
@@ -37,10 +35,9 @@ def remove_headers(request, headers_to_remove):
|
||||
|
||||
|
||||
def replace_query_parameters(request, replacements):
|
||||
"""
|
||||
Replace query parameters in request according to replacements. The
|
||||
replacements should be a list of (key, value) pairs where the value can be
|
||||
any of:
|
||||
"""Replace query parameters in request according to replacements.
|
||||
|
||||
The replacements should be a list of (key, value) pairs where the value can be any of:
|
||||
1. A simple replacement string value.
|
||||
2. None to remove the given header.
|
||||
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):
|
||||
"""
|
||||
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 value can be any of:
|
||||
"""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 value can be any of:
|
||||
1. A simple replacement string value.
|
||||
2. None to remove the given header.
|
||||
3. A callable which accepts (key, value, request) and returns a string
|
||||
@@ -88,8 +84,18 @@ def replace_post_data_parameters(request, replacements):
|
||||
|
||||
replacements = dict(replacements)
|
||||
if request.method == "POST" and not isinstance(request.body, BytesIO):
|
||||
if request.headers.get("Content-Type") == "application/json":
|
||||
json_data = json.loads(request.body.decode("utf-8"))
|
||||
if isinstance(request.body, dict):
|
||||
new_body = request.body.copy()
|
||||
for k, rv in replacements.items():
|
||||
if k in new_body:
|
||||
ov = new_body.pop(k)
|
||||
if callable(rv):
|
||||
rv = rv(key=k, value=ov, request=request)
|
||||
if rv is not None:
|
||||
new_body[k] = rv
|
||||
request.body = new_body
|
||||
elif request.headers.get("Content-Type") == "application/json":
|
||||
json_data = json.loads(request.body)
|
||||
for k, rv in replacements.items():
|
||||
if k in json_data:
|
||||
ov = json_data.pop(k)
|
||||
@@ -99,7 +105,7 @@ def replace_post_data_parameters(request, replacements):
|
||||
json_data[k] = rv
|
||||
request.body = json.dumps(json_data).encode("utf-8")
|
||||
else:
|
||||
if isinstance(request.body, text_type):
|
||||
if isinstance(request.body, str):
|
||||
request.body = request.body.encode("utf-8")
|
||||
splits = [p.partition(b"=") for p in request.body.split(b"&")]
|
||||
new_splits = []
|
||||
@@ -144,10 +150,18 @@ def decode_response(response):
|
||||
"""Returns decompressed body according to encoding using zlib.
|
||||
to (de-)compress gzip format, use wbits = zlib.MAX_WBITS | 16
|
||||
"""
|
||||
if not body:
|
||||
return ""
|
||||
if encoding == "gzip":
|
||||
return zlib.decompress(body, zlib.MAX_WBITS | 16)
|
||||
try:
|
||||
return zlib.decompress(body, zlib.MAX_WBITS | 16)
|
||||
except zlib.error:
|
||||
return body # assumes that the data was already decompressed
|
||||
else: # encoding == 'deflate'
|
||||
return zlib.decompress(body)
|
||||
try:
|
||||
return zlib.decompress(body)
|
||||
except zlib.error:
|
||||
return body # assumes that the data was already decompressed
|
||||
|
||||
# Deepcopy here in case `headers` contain objects that could
|
||||
# be mutated by a shallow copy and corrupt the real response.
|
||||
|
||||
129
vcr/matchers.py
129
vcr/matchers.py
@@ -1,54 +1,75 @@
|
||||
import json
|
||||
from six.moves import urllib, xmlrpc_client
|
||||
from .util import read_body
|
||||
import logging
|
||||
import urllib
|
||||
import xmlrpc.client
|
||||
from string import hexdigits
|
||||
from typing import List, Set
|
||||
|
||||
from .util import read_body
|
||||
|
||||
_HEXDIG_CODE_POINTS: Set[int] = {ord(s.encode("ascii")) for s in hexdigits}
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def method(r1, r2):
|
||||
assert r1.method == r2.method, "{} != {}".format(r1.method, r2.method)
|
||||
if r1.method != r2.method:
|
||||
raise AssertionError(f"{r1.method} != {r2.method}")
|
||||
|
||||
|
||||
def uri(r1, r2):
|
||||
assert r1.uri == r2.uri, "{} != {}".format(r1.uri, r2.uri)
|
||||
if r1.uri != r2.uri:
|
||||
raise AssertionError(f"{r1.uri} != {r2.uri}")
|
||||
|
||||
|
||||
def host(r1, r2):
|
||||
assert r1.host == r2.host, "{} != {}".format(r1.host, r2.host)
|
||||
if r1.host != r2.host:
|
||||
raise AssertionError(f"{r1.host} != {r2.host}")
|
||||
|
||||
|
||||
def scheme(r1, r2):
|
||||
assert r1.scheme == r2.scheme, "{} != {}".format(r1.scheme, r2.scheme)
|
||||
if r1.scheme != r2.scheme:
|
||||
raise AssertionError(f"{r1.scheme} != {r2.scheme}")
|
||||
|
||||
|
||||
def port(r1, r2):
|
||||
assert r1.port == r2.port, "{} != {}".format(r1.port, r2.port)
|
||||
if r1.port != r2.port:
|
||||
raise AssertionError(f"{r1.port} != {r2.port}")
|
||||
|
||||
|
||||
def path(r1, r2):
|
||||
assert r1.path == r2.path, "{} != {}".format(r1.path, r2.path)
|
||||
if r1.path != r2.path:
|
||||
raise AssertionError(f"{r1.path} != {r2.path}")
|
||||
|
||||
|
||||
def query(r1, r2):
|
||||
assert r1.query == r2.query, "{} != {}".format(r1.query, r2.query)
|
||||
if r1.query != r2.query:
|
||||
raise AssertionError(f"{r1.query} != {r2.query}")
|
||||
|
||||
|
||||
def raw_body(r1, r2):
|
||||
assert read_body(r1) == read_body(r2)
|
||||
if read_body(r1) != read_body(r2):
|
||||
raise AssertionError
|
||||
|
||||
|
||||
def body(r1, r2):
|
||||
transformer = _get_transformer(r1)
|
||||
r2_transformer = _get_transformer(r2)
|
||||
if transformer != r2_transformer:
|
||||
transformer = _identity
|
||||
assert transformer(read_body(r1)) == transformer(read_body(r2))
|
||||
transformers = list(_get_transformers(r1))
|
||||
if transformers != list(_get_transformers(r2)):
|
||||
transformers = []
|
||||
|
||||
b1 = read_body(r1)
|
||||
b2 = read_body(r2)
|
||||
for transform in transformers:
|
||||
b1 = transform(b1)
|
||||
b2 = transform(b2)
|
||||
|
||||
if b1 != b2:
|
||||
raise AssertionError
|
||||
|
||||
|
||||
def headers(r1, r2):
|
||||
assert r1.headers == r2.headers, "{} != {}".format(r1.headers, r2.headers)
|
||||
if r1.headers != r2.headers:
|
||||
raise AssertionError(f"{r1.headers} != {r2.headers}")
|
||||
|
||||
|
||||
def _header_checker(value, header="Content-Type"):
|
||||
@@ -61,42 +82,90 @@ def _header_checker(value, header="Content-Type"):
|
||||
return checker
|
||||
|
||||
|
||||
def _dechunk(body):
|
||||
if isinstance(body, str):
|
||||
body = body.encode("utf-8")
|
||||
elif isinstance(body, bytearray):
|
||||
body = bytes(body)
|
||||
elif hasattr(body, "__iter__"):
|
||||
body = list(body)
|
||||
if body:
|
||||
if isinstance(body[0], str):
|
||||
body = ("".join(body)).encode("utf-8")
|
||||
elif isinstance(body[0], bytes):
|
||||
body = b"".join(body)
|
||||
elif isinstance(body[0], int):
|
||||
body = bytes(body)
|
||||
else:
|
||||
raise ValueError(f"Body chunk type {type(body[0])} not supported")
|
||||
else:
|
||||
body = None
|
||||
|
||||
if not isinstance(body, bytes):
|
||||
return body
|
||||
|
||||
# Now decode chunked data format (https://en.wikipedia.org/wiki/Chunked_transfer_encoding)
|
||||
# Example input: b"45\r\n<69 bytes>\r\n0\r\n\r\n" where int(b"45", 16) == 69.
|
||||
CHUNK_GAP = b"\r\n"
|
||||
BODY_LEN: int = len(body)
|
||||
|
||||
chunks: List[bytes] = []
|
||||
pos: int = 0
|
||||
|
||||
while True:
|
||||
for i in range(pos, BODY_LEN):
|
||||
if body[i] not in _HEXDIG_CODE_POINTS:
|
||||
break
|
||||
|
||||
if i == 0 or body[i : i + len(CHUNK_GAP)] != CHUNK_GAP:
|
||||
if pos == 0:
|
||||
return body # i.e. assume non-chunk data
|
||||
raise ValueError("Malformed chunked data")
|
||||
|
||||
size_bytes = int(body[pos:i], 16)
|
||||
if size_bytes == 0: # i.e. well-formed ending
|
||||
return b"".join(chunks)
|
||||
|
||||
chunk_data_first = i + len(CHUNK_GAP)
|
||||
chunk_data_after_last = chunk_data_first + size_bytes
|
||||
|
||||
if body[chunk_data_after_last : chunk_data_after_last + len(CHUNK_GAP)] != CHUNK_GAP:
|
||||
raise ValueError("Malformed chunked data")
|
||||
|
||||
chunk_data = body[chunk_data_first:chunk_data_after_last]
|
||||
chunks.append(chunk_data)
|
||||
|
||||
pos = chunk_data_after_last + len(CHUNK_GAP)
|
||||
|
||||
|
||||
def _transform_json(body):
|
||||
# Request body is always a byte string, but json.loads() wants a text
|
||||
# string. RFC 7159 says the default encoding is UTF-8 (although UTF-16
|
||||
# and UTF-32 are also allowed: hmmmmm).
|
||||
if body:
|
||||
return json.loads(body.decode("utf-8"))
|
||||
return json.loads(body)
|
||||
|
||||
|
||||
_xml_header_checker = _header_checker("text/xml")
|
||||
_xmlrpc_header_checker = _header_checker("xmlrpc", header="User-Agent")
|
||||
_checker_transformer_pairs = (
|
||||
(_header_checker("chunked", header="Transfer-Encoding"), _dechunk),
|
||||
(
|
||||
_header_checker("application/x-www-form-urlencoded"),
|
||||
lambda body: urllib.parse.parse_qs(body.decode("ascii")),
|
||||
),
|
||||
(_header_checker("application/json"), _transform_json),
|
||||
(lambda request: _xml_header_checker(request) and _xmlrpc_header_checker(request), xmlrpc_client.loads),
|
||||
(lambda request: _xml_header_checker(request) and _xmlrpc_header_checker(request), xmlrpc.client.loads),
|
||||
)
|
||||
|
||||
|
||||
def _identity(x):
|
||||
return x
|
||||
|
||||
|
||||
def _get_transformer(request):
|
||||
def _get_transformers(request):
|
||||
for checker, transformer in _checker_transformer_pairs:
|
||||
if checker(request.headers):
|
||||
return transformer
|
||||
else:
|
||||
return _identity
|
||||
yield transformer
|
||||
|
||||
|
||||
def requests_match(r1, r2, matchers):
|
||||
successes, failures = get_matchers_results(r1, r2, matchers)
|
||||
if failures:
|
||||
log.debug("Requests {} and {} differ.\n" "Failure details:\n" "{}".format(r1, r2, failures))
|
||||
log.debug(f"Requests {r1} and {r2} differ.\nFailure details:\n{failures}")
|
||||
return len(failures) == 0
|
||||
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ It merges and deletes the request obsolete keys (protocol, host, port, path)
|
||||
into new 'uri' key.
|
||||
Usage::
|
||||
|
||||
python -m vcr.migration PATH
|
||||
python3 -m vcr.migration PATH
|
||||
|
||||
The PATH can be path to the directory with cassettes or cassette itself
|
||||
"""
|
||||
@@ -17,11 +17,12 @@ import os
|
||||
import shutil
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
import yaml
|
||||
|
||||
from .serializers import yamlserializer, jsonserializer
|
||||
from .serialize import serialize
|
||||
from . import request
|
||||
from .serialize import serialize
|
||||
from .serializers import jsonserializer, yamlserializer
|
||||
from .stubs.compat import get_httpmessage
|
||||
|
||||
# Use the libYAML versions if possible
|
||||
@@ -54,7 +55,7 @@ def build_uri(**parts):
|
||||
port = parts["port"]
|
||||
scheme = parts["protocol"]
|
||||
default_port = {"https": 443, "http": 80}[scheme]
|
||||
parts["port"] = ":{}".format(port) if port != default_port else ""
|
||||
parts["port"] = f":{port}" if port != default_port else ""
|
||||
return "{protocol}://{host}{port}{path}".format(**parts)
|
||||
|
||||
|
||||
@@ -91,7 +92,7 @@ def migrate_json(in_fp, out_fp):
|
||||
|
||||
|
||||
def _list_of_tuples_to_dict(fs):
|
||||
return {k: v for k, v in fs[0]}
|
||||
return dict(fs[0])
|
||||
|
||||
|
||||
def _already_migrated(data):
|
||||
@@ -117,7 +118,7 @@ def migrate(file_path, migration_fn):
|
||||
# because we assume that original files can be reverted
|
||||
# we will try to copy the content. (os.rename not needed)
|
||||
with tempfile.TemporaryFile(mode="w+") as out_fp:
|
||||
with open(file_path, "r") as in_fp:
|
||||
with open(file_path) as in_fp:
|
||||
if not migration_fn(in_fp, out_fp):
|
||||
return False
|
||||
with open(file_path, "w") as in_fp:
|
||||
@@ -129,7 +130,7 @@ def migrate(file_path, migration_fn):
|
||||
def try_migrate(path):
|
||||
if path.endswith(".json"):
|
||||
return migrate(path, migrate_json)
|
||||
elif path.endswith(".yaml") or path.endswith(".yml"):
|
||||
elif path.endswith((".yaml", ".yml")):
|
||||
return migrate(path, migrate_yml)
|
||||
return False
|
||||
|
||||
@@ -137,7 +138,7 @@ def try_migrate(path):
|
||||
def main():
|
||||
if len(sys.argv) != 2:
|
||||
raise SystemExit(
|
||||
"Please provide path to cassettes directory or file. " "Usage: python -m vcr.migration PATH"
|
||||
"Please provide path to cassettes directory or file. Usage: python3 -m vcr.migration PATH",
|
||||
)
|
||||
|
||||
path = sys.argv[1]
|
||||
@@ -149,7 +150,7 @@ def main():
|
||||
for file_path in files:
|
||||
migrated = try_migrate(file_path)
|
||||
status = "OK" if migrated else "FAIL"
|
||||
sys.stderr.write("[{}] {}\n".format(status, file_path))
|
||||
sys.stderr.write(f"[{status}] {file_path}\n")
|
||||
sys.stderr.write("Done.\n")
|
||||
|
||||
|
||||
|
||||
171
vcr/patch.py
171
vcr/patch.py
@@ -1,12 +1,12 @@
|
||||
"""Utilities for patching in cassettes"""
|
||||
import contextlib
|
||||
import functools
|
||||
import http.client as httplib
|
||||
import itertools
|
||||
|
||||
from .compat import contextlib, mock
|
||||
from .stubs import VCRHTTPConnection, VCRHTTPSConnection
|
||||
from six.moves import http_client as httplib
|
||||
|
||||
import logging
|
||||
from unittest import mock
|
||||
|
||||
from .stubs import VCRHTTPConnection, VCRHTTPSConnection
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
# Save some of the original types for the purposes of unpatching
|
||||
@@ -15,42 +15,47 @@ _HTTPSConnection = httplib.HTTPSConnection
|
||||
|
||||
# Try to save the original types for boto3
|
||||
try:
|
||||
from botocore.awsrequest import AWSHTTPSConnection, AWSHTTPConnection
|
||||
except ImportError:
|
||||
from botocore.awsrequest import AWSHTTPConnection, AWSHTTPSConnection
|
||||
except ImportError as e:
|
||||
try:
|
||||
import botocore.vendored.requests.packages.urllib3.connectionpool as cpool
|
||||
import botocore.vendored.requests # noqa: F401
|
||||
except ImportError: # pragma: no cover
|
||||
pass
|
||||
else:
|
||||
_Boto3VerifiedHTTPSConnection = cpool.VerifiedHTTPSConnection
|
||||
_cpoolBoto3HTTPConnection = cpool.HTTPConnection
|
||||
_cpoolBoto3HTTPSConnection = cpool.HTTPSConnection
|
||||
raise RuntimeError(
|
||||
"vcrpy >=4.2.2 and botocore <1.11.0 are not compatible"
|
||||
"; please upgrade botocore (or downgrade vcrpy)",
|
||||
) from e
|
||||
else:
|
||||
_Boto3VerifiedHTTPSConnection = AWSHTTPSConnection
|
||||
_cpoolBoto3HTTPConnection = AWSHTTPConnection
|
||||
_cpoolBoto3HTTPSConnection = AWSHTTPSConnection
|
||||
|
||||
cpool = None
|
||||
conn = None
|
||||
# Try to save the original types for urllib3
|
||||
try:
|
||||
import urllib3.connection as conn
|
||||
import urllib3.connectionpool as cpool
|
||||
except ImportError: # pragma: no cover
|
||||
pass
|
||||
else:
|
||||
_VerifiedHTTPSConnection = cpool.VerifiedHTTPSConnection
|
||||
_cpoolHTTPConnection = cpool.HTTPConnection
|
||||
_cpoolHTTPSConnection = cpool.HTTPSConnection
|
||||
_VerifiedHTTPSConnection = conn.VerifiedHTTPSConnection
|
||||
_connHTTPConnection = conn.HTTPConnection
|
||||
_connHTTPSConnection = conn.HTTPSConnection
|
||||
|
||||
# Try to save the original types for requests
|
||||
try:
|
||||
if not cpool:
|
||||
import requests.packages.urllib3.connectionpool as cpool
|
||||
import requests
|
||||
except ImportError: # pragma: no cover
|
||||
pass
|
||||
else:
|
||||
_VerifiedHTTPSConnection = cpool.VerifiedHTTPSConnection
|
||||
_cpoolHTTPConnection = cpool.HTTPConnection
|
||||
_cpoolHTTPSConnection = cpool.HTTPSConnection
|
||||
if requests.__build__ < 0x021602:
|
||||
raise RuntimeError(
|
||||
"vcrpy >=4.2.2 and requests <2.16.2 are not compatible"
|
||||
"; please upgrade requests (or downgrade vcrpy)",
|
||||
)
|
||||
|
||||
|
||||
# Try to save the original types for httplib2
|
||||
try:
|
||||
@@ -93,7 +98,16 @@ else:
|
||||
_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):
|
||||
@functools.wraps(function)
|
||||
def wrapped(self, *args, **kwargs):
|
||||
@@ -115,6 +129,7 @@ class CassettePatcherBuilder(object):
|
||||
self._boto(),
|
||||
self._tornado(),
|
||||
self._aiohttp(),
|
||||
self._httpx(),
|
||||
self._build_patchers_from_mock_triples(self._cassette.custom_patches),
|
||||
)
|
||||
|
||||
@@ -129,7 +144,9 @@ class CassettePatcherBuilder(object):
|
||||
return
|
||||
|
||||
return mock.patch.object(
|
||||
obj, patched_attribute, self._recursively_apply_get_cassette_subclass(replacement_class)
|
||||
obj,
|
||||
patched_attribute,
|
||||
self._recursively_apply_get_cassette_subclass(replacement_class),
|
||||
)
|
||||
|
||||
def _recursively_apply_get_cassette_subclass(self, replacement_dict_or_obj):
|
||||
@@ -171,9 +188,7 @@ class CassettePatcherBuilder(object):
|
||||
bases = (base_class,)
|
||||
if not issubclass(base_class, object): # Check for old style class
|
||||
bases += (object,)
|
||||
return type(
|
||||
"{}{}".format(base_class.__name__, self._cassette._path), bases, dict(cassette=self._cassette)
|
||||
)
|
||||
return type(f"{base_class.__name__}{self._cassette._path}", bases, {"cassette": self._cassette})
|
||||
|
||||
@_build_patchers_from_mock_triples_decorator
|
||||
def _httplib(self):
|
||||
@@ -185,24 +200,15 @@ class CassettePatcherBuilder(object):
|
||||
from .stubs import requests_stubs
|
||||
except ImportError: # pragma: no cover
|
||||
return ()
|
||||
return self._urllib3_patchers(cpool, requests_stubs)
|
||||
return self._urllib3_patchers(cpool, conn, requests_stubs)
|
||||
|
||||
@_build_patchers_from_mock_triples_decorator
|
||||
def _boto3(self):
|
||||
|
||||
try:
|
||||
# botocore using awsrequest
|
||||
import botocore.awsrequest as cpool
|
||||
except ImportError: # pragma: no cover
|
||||
try:
|
||||
# botocore using vendored requests
|
||||
import botocore.vendored.requests.packages.urllib3.connectionpool as cpool
|
||||
except ImportError: # pragma: no cover
|
||||
pass
|
||||
else:
|
||||
from .stubs import boto3_stubs
|
||||
|
||||
yield self._urllib3_patchers(cpool, boto3_stubs)
|
||||
pass
|
||||
else:
|
||||
from .stubs import boto3_stubs
|
||||
|
||||
@@ -244,12 +250,13 @@ class CassettePatcherBuilder(object):
|
||||
|
||||
def _urllib3(self):
|
||||
try:
|
||||
import urllib3.connection as conn
|
||||
import urllib3.connectionpool as cpool
|
||||
except ImportError: # pragma: no cover
|
||||
return ()
|
||||
from .stubs import urllib3_stubs
|
||||
|
||||
return self._urllib3_patchers(cpool, urllib3_stubs)
|
||||
return self._urllib3_patchers(cpool, conn, urllib3_stubs)
|
||||
|
||||
@_build_patchers_from_mock_triples_decorator
|
||||
def _httplib2(self):
|
||||
@@ -258,8 +265,7 @@ class CassettePatcherBuilder(object):
|
||||
except ImportError: # pragma: no cover
|
||||
pass
|
||||
else:
|
||||
from .stubs.httplib2_stubs import VCRHTTPConnectionWithTimeout
|
||||
from .stubs.httplib2_stubs import VCRHTTPSConnectionWithTimeout
|
||||
from .stubs.httplib2_stubs import VCRHTTPConnectionWithTimeout, VCRHTTPSConnectionWithTimeout
|
||||
|
||||
yield cpool, "HTTPConnectionWithTimeout", VCRHTTPConnectionWithTimeout
|
||||
yield cpool, "HTTPSConnectionWithTimeout", VCRHTTPSConnectionWithTimeout
|
||||
@@ -312,17 +318,32 @@ class CassettePatcherBuilder(object):
|
||||
new_request = vcr_request(self._cassette, _AiohttpClientSessionRequest)
|
||||
yield client.ClientSession, "_request", new_request
|
||||
|
||||
def _urllib3_patchers(self, cpool, stubs):
|
||||
@_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, conn, stubs):
|
||||
http_connection_remover = ConnectionRemover(
|
||||
self._get_cassette_subclass(stubs.VCRRequestsHTTPConnection)
|
||||
self._get_cassette_subclass(stubs.VCRRequestsHTTPConnection),
|
||||
)
|
||||
https_connection_remover = ConnectionRemover(
|
||||
self._get_cassette_subclass(stubs.VCRRequestsHTTPSConnection)
|
||||
self._get_cassette_subclass(stubs.VCRRequestsHTTPSConnection),
|
||||
)
|
||||
mock_triples = (
|
||||
(cpool, "VerifiedHTTPSConnection", stubs.VCRRequestsHTTPSConnection),
|
||||
(cpool, "HTTPConnection", stubs.VCRRequestsHTTPConnection),
|
||||
(cpool, "HTTPSConnection", stubs.VCRRequestsHTTPSConnection),
|
||||
(conn, "VerifiedHTTPSConnection", stubs.VCRRequestsHTTPSConnection),
|
||||
(conn, "HTTPConnection", stubs.VCRRequestsHTTPConnection),
|
||||
(conn, "HTTPSConnection", stubs.VCRRequestsHTTPSConnection),
|
||||
(cpool, "is_connection_dropped", mock.Mock(return_value=False)), # Needed on Windows only
|
||||
(cpool.HTTPConnectionPool, "ConnectionCls", stubs.VCRRequestsHTTPConnection),
|
||||
(cpool.HTTPSConnectionPool, "ConnectionCls", stubs.VCRRequestsHTTPSConnection),
|
||||
@@ -358,7 +379,7 @@ class CassettePatcherBuilder(object):
|
||||
)
|
||||
|
||||
|
||||
class ConnectionRemover(object):
|
||||
class ConnectionRemover:
|
||||
def __init__(self, connection_class):
|
||||
self._connection_class = connection_class
|
||||
self._connection_pool_to_connections = {}
|
||||
@@ -392,69 +413,23 @@ def reset_patchers():
|
||||
yield mock.patch.object(httplib, "HTTPSConnection", _HTTPSConnection)
|
||||
|
||||
try:
|
||||
import requests
|
||||
|
||||
if requests.__build__ < 0x021603:
|
||||
# Avoid double unmock if requests 2.16.3
|
||||
# First, this is pointless, requests.packages.urllib3 *IS* urllib3 (see packages.py)
|
||||
# Second, this is unmocking twice the same classes with different namespaces
|
||||
# and is creating weird issues and bugs:
|
||||
# > AssertionError: assert <class 'urllib3.connection.HTTPConnection'>
|
||||
# > is <class 'requests.packages.urllib3.connection.HTTPConnection'>
|
||||
# This assert should work!!!
|
||||
# Note that this also means that now, requests.packages is never imported
|
||||
# if requests 2.16.3 or greater is used with VCRPy.
|
||||
import requests.packages.urllib3.connectionpool as cpool
|
||||
else:
|
||||
raise ImportError("Skip requests not vendored anymore")
|
||||
except ImportError: # pragma: no cover
|
||||
pass
|
||||
else:
|
||||
# unpatch requests v1.x
|
||||
yield mock.patch.object(cpool, "VerifiedHTTPSConnection", _VerifiedHTTPSConnection)
|
||||
yield mock.patch.object(cpool, "HTTPConnection", _cpoolHTTPConnection)
|
||||
# unpatch requests v2.x
|
||||
if hasattr(cpool.HTTPConnectionPool, "ConnectionCls"):
|
||||
yield mock.patch.object(cpool.HTTPConnectionPool, "ConnectionCls", _cpoolHTTPConnection)
|
||||
yield mock.patch.object(cpool.HTTPSConnectionPool, "ConnectionCls", _cpoolHTTPSConnection)
|
||||
|
||||
if hasattr(cpool, "HTTPSConnection"):
|
||||
yield mock.patch.object(cpool, "HTTPSConnection", _cpoolHTTPSConnection)
|
||||
|
||||
try:
|
||||
import urllib3.connection as conn
|
||||
import urllib3.connectionpool as cpool
|
||||
except ImportError: # pragma: no cover
|
||||
pass
|
||||
else:
|
||||
yield mock.patch.object(cpool, "VerifiedHTTPSConnection", _VerifiedHTTPSConnection)
|
||||
yield mock.patch.object(cpool, "HTTPConnection", _cpoolHTTPConnection)
|
||||
yield mock.patch.object(cpool, "HTTPSConnection", _cpoolHTTPSConnection)
|
||||
yield mock.patch.object(conn, "VerifiedHTTPSConnection", _VerifiedHTTPSConnection)
|
||||
yield mock.patch.object(conn, "HTTPConnection", _connHTTPConnection)
|
||||
yield mock.patch.object(conn, "HTTPSConnection", _connHTTPSConnection)
|
||||
if hasattr(cpool.HTTPConnectionPool, "ConnectionCls"):
|
||||
yield mock.patch.object(cpool.HTTPConnectionPool, "ConnectionCls", _cpoolHTTPConnection)
|
||||
yield mock.patch.object(cpool.HTTPSConnectionPool, "ConnectionCls", _cpoolHTTPSConnection)
|
||||
yield mock.patch.object(cpool.HTTPConnectionPool, "ConnectionCls", _connHTTPConnection)
|
||||
yield mock.patch.object(cpool.HTTPSConnectionPool, "ConnectionCls", _connHTTPSConnection)
|
||||
|
||||
try:
|
||||
# unpatch botocore with awsrequest
|
||||
import botocore.awsrequest as cpool
|
||||
except ImportError: # pragma: no cover
|
||||
try:
|
||||
# unpatch botocore with vendored requests
|
||||
import botocore.vendored.requests.packages.urllib3.connectionpool as cpool
|
||||
except ImportError: # pragma: no cover
|
||||
pass
|
||||
else:
|
||||
# unpatch requests v1.x
|
||||
yield mock.patch.object(cpool, "VerifiedHTTPSConnection", _Boto3VerifiedHTTPSConnection)
|
||||
yield mock.patch.object(cpool, "HTTPConnection", _cpoolBoto3HTTPConnection)
|
||||
# unpatch requests v2.x
|
||||
if hasattr(cpool.HTTPConnectionPool, "ConnectionCls"):
|
||||
yield mock.patch.object(cpool.HTTPConnectionPool, "ConnectionCls", _cpoolBoto3HTTPConnection)
|
||||
yield mock.patch.object(
|
||||
cpool.HTTPSConnectionPool, "ConnectionCls", _cpoolBoto3HTTPSConnection
|
||||
)
|
||||
|
||||
if hasattr(cpool, "HTTPSConnection"):
|
||||
yield mock.patch.object(cpool, "HTTPSConnection", _cpoolBoto3HTTPSConnection)
|
||||
pass
|
||||
else:
|
||||
if hasattr(cpool.AWSHTTPConnectionPool, "ConnectionCls"):
|
||||
yield mock.patch.object(cpool.AWSHTTPConnectionPool, "ConnectionCls", _cpoolBoto3HTTPConnection)
|
||||
|
||||
@@ -1,25 +1,40 @@
|
||||
# .. _persister_example:
|
||||
|
||||
import os
|
||||
from ..serialize import serialize, deserialize
|
||||
from pathlib import Path
|
||||
|
||||
from ..serialize import deserialize, serialize
|
||||
|
||||
|
||||
class FilesystemPersister(object):
|
||||
class CassetteNotFoundError(FileNotFoundError):
|
||||
pass
|
||||
|
||||
|
||||
class CassetteDecodeError(ValueError):
|
||||
pass
|
||||
|
||||
|
||||
class FilesystemPersister:
|
||||
@classmethod
|
||||
def load_cassette(cls, cassette_path, serializer):
|
||||
cassette_path = Path(cassette_path) # if cassette path is already Path this is no operation
|
||||
if not cassette_path.is_file():
|
||||
raise CassetteNotFoundError()
|
||||
try:
|
||||
with open(cassette_path) as f:
|
||||
cassette_content = f.read()
|
||||
except IOError:
|
||||
raise ValueError("Cassette not found.")
|
||||
cassette = deserialize(cassette_content, serializer)
|
||||
return cassette
|
||||
with cassette_path.open() as f:
|
||||
data = f.read()
|
||||
except UnicodeDecodeError as err:
|
||||
raise CassetteDecodeError("Can't read Cassette, Encoding is broken") from err
|
||||
|
||||
return deserialize(data, serializer)
|
||||
|
||||
@staticmethod
|
||||
def save_cassette(cassette_path, cassette_dict, serializer):
|
||||
data = serialize(cassette_dict, serializer)
|
||||
dirname, filename = os.path.split(cassette_path)
|
||||
if dirname and not os.path.exists(dirname):
|
||||
os.makedirs(dirname)
|
||||
with open(cassette_path, "w") as f:
|
||||
cassette_path = Path(cassette_path) # if cassette path is already Path this is no operation
|
||||
|
||||
cassette_folder = cassette_path.parent
|
||||
if not cassette_folder.exists():
|
||||
cassette_folder.mkdir(parents=True)
|
||||
|
||||
with cassette_path.open("w") as f:
|
||||
f.write(data)
|
||||
|
||||
23
vcr/record_mode.py
Normal file
23
vcr/record_mode.py
Normal file
@@ -0,0 +1,23 @@
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class RecordMode(str, Enum):
|
||||
"""
|
||||
Configures 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"
|
||||
@@ -1,13 +1,14 @@
|
||||
import warnings
|
||||
from six import BytesIO, text_type
|
||||
from six.moves.urllib.parse import urlparse, parse_qsl
|
||||
from .util import CaseInsensitiveDict
|
||||
import logging
|
||||
import warnings
|
||||
from io import BytesIO
|
||||
from urllib.parse import parse_qsl, urlparse
|
||||
|
||||
from .util import CaseInsensitiveDict
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Request(object):
|
||||
class Request:
|
||||
"""
|
||||
VCR's representation of a request.
|
||||
"""
|
||||
@@ -39,14 +40,15 @@ class Request(object):
|
||||
|
||||
@body.setter
|
||||
def body(self, value):
|
||||
if isinstance(value, text_type):
|
||||
if isinstance(value, str):
|
||||
value = value.encode("utf-8")
|
||||
self._body = value
|
||||
|
||||
def add_header(self, key, value):
|
||||
warnings.warn(
|
||||
"Request.add_header is deprecated. " "Please assign to request.headers instead.",
|
||||
"Request.add_header is deprecated. Please assign to request.headers instead.",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
self.headers[key] = value
|
||||
|
||||
@@ -89,7 +91,7 @@ class Request(object):
|
||||
return self.scheme
|
||||
|
||||
def __str__(self):
|
||||
return "<Request ({}) {}>".format(self.method, self.uri)
|
||||
return f"<Request ({self.method}) {self.uri}>"
|
||||
|
||||
def __repr__(self):
|
||||
return self.__str__()
|
||||
@@ -136,4 +138,4 @@ class HeadersDict(CaseInsensitiveDict):
|
||||
if old:
|
||||
key = old[0]
|
||||
|
||||
super(HeadersDict, self).__setitem__(key, value)
|
||||
super().__setitem__(key, value)
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
from vcr.serializers import compat
|
||||
from vcr.request import Request
|
||||
import yaml
|
||||
|
||||
from vcr.request import Request
|
||||
from vcr.serializers import compat
|
||||
|
||||
# version 1 cassettes started with VCR 1.0.x.
|
||||
# Before 1.0.x, there was no versioning.
|
||||
CASSETTE_FORMAT_VERSION = 1
|
||||
@@ -27,7 +28,7 @@ def _warn_about_old_cassette_format():
|
||||
raise ValueError(
|
||||
"Your cassette files were generated in an older version "
|
||||
"of VCR. Delete your cassettes or run the migration script."
|
||||
"See http://git.io/mHhLBg for more details."
|
||||
"See http://git.io/mHhLBg for more details.",
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
import six
|
||||
|
||||
|
||||
def convert_to_bytes(resp):
|
||||
resp = convert_body_to_bytes(resp)
|
||||
return resp
|
||||
@@ -24,7 +21,7 @@ def convert_body_to_bytes(resp):
|
||||
http://pyyaml.org/wiki/PyYAMLDocumentation#Python3support
|
||||
"""
|
||||
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")
|
||||
except (KeyError, TypeError, UnicodeEncodeError):
|
||||
# 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
|
||||
|
||||
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")
|
||||
except (TypeError, UnicodeDecodeError, AttributeError):
|
||||
# Sometimes the string actually is binary or StringIO object,
|
||||
|
||||
@@ -16,14 +16,6 @@ def serialize(cassette_dict):
|
||||
)
|
||||
|
||||
try:
|
||||
return json.dumps(cassette_dict, indent=4)
|
||||
except UnicodeDecodeError as original: # py2
|
||||
raise UnicodeDecodeError(
|
||||
original.encoding,
|
||||
b"Error serializing cassette to JSON",
|
||||
original.start,
|
||||
original.end,
|
||||
original.args[-1] + error_message,
|
||||
)
|
||||
except TypeError: # py3
|
||||
raise TypeError(error_message)
|
||||
return json.dumps(cassette_dict, indent=4) + "\n"
|
||||
except TypeError:
|
||||
raise TypeError(error_message) from None
|
||||
|
||||
@@ -2,9 +2,10 @@ import yaml
|
||||
|
||||
# Use the libYAML versions if possible
|
||||
try:
|
||||
from yaml import CLoader as Loader, CDumper as Dumper
|
||||
from yaml import CDumper as Dumper
|
||||
from yaml import CLoader as Loader
|
||||
except ImportError:
|
||||
from yaml import Loader, Dumper
|
||||
from yaml import Dumper, Loader
|
||||
|
||||
|
||||
def deserialize(cassette_string):
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
"""Stubs for patching HTTP and HTTPS requests"""
|
||||
|
||||
import logging
|
||||
import six
|
||||
from six.moves.http_client import HTTPConnection, HTTPSConnection, HTTPResponse
|
||||
from six import BytesIO
|
||||
from vcr.request import Request
|
||||
from http.client import HTTPConnection, HTTPResponse, HTTPSConnection
|
||||
from io import BytesIO
|
||||
|
||||
from vcr.errors import CannotOverwriteExistingCassetteException
|
||||
from vcr.request import Request
|
||||
|
||||
from . import compat
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class VCRFakeSocket(object):
|
||||
class VCRFakeSocket:
|
||||
"""
|
||||
A socket that doesn't do anything!
|
||||
Used when playing back cassettes, when there
|
||||
@@ -46,8 +47,9 @@ def parse_headers(header_list):
|
||||
|
||||
|
||||
def serialize_headers(response):
|
||||
headers = response.headers if response.msg is None else response.msg
|
||||
out = {}
|
||||
for key, values in compat.get_headers(response.msg):
|
||||
for key, values in compat.get_headers(headers):
|
||||
out.setdefault(key, [])
|
||||
out[key].extend(values)
|
||||
return out
|
||||
@@ -66,6 +68,7 @@ class VCRHTTPResponse(HTTPResponse):
|
||||
self.version = None
|
||||
self._content = BytesIO(self.recorded_response["body"]["string"])
|
||||
self._closed = False
|
||||
self._original_response = self # for requests.session.Session cookie extraction
|
||||
|
||||
headers = self.recorded_response["headers"]
|
||||
# Since we are loading a response that has already been serialized, our
|
||||
@@ -84,12 +87,15 @@ class VCRHTTPResponse(HTTPResponse):
|
||||
def closed(self):
|
||||
# in python3, I can't change the value of self.closed. So I'
|
||||
# twiddling self._closed and using this property to shadow the real
|
||||
# self.closed from the superclas
|
||||
# self.closed from the superclass
|
||||
return self._closed
|
||||
|
||||
def read(self, *args, **kwargs):
|
||||
return self._content.read(*args, **kwargs)
|
||||
|
||||
def read1(self, *args, **kwargs):
|
||||
return self._content.read1(*args, **kwargs)
|
||||
|
||||
def readall(self):
|
||||
return self._content.readall()
|
||||
|
||||
@@ -142,8 +148,37 @@ class VCRHTTPResponse(HTTPResponse):
|
||||
def readable(self):
|
||||
return self._content.readable()
|
||||
|
||||
@property
|
||||
def length_remaining(self):
|
||||
return self._content.getbuffer().nbytes - self._content.tell()
|
||||
|
||||
class VCRConnection(object):
|
||||
def get_redirect_location(self):
|
||||
"""
|
||||
Returns (a) redirect location string if we got a redirect
|
||||
status code and valid location, (b) None if redirect status and
|
||||
no location, (c) False if not a redirect status code.
|
||||
See https://urllib3.readthedocs.io/en/stable/reference/urllib3.response.html .
|
||||
"""
|
||||
if not (300 <= self.status <= 399):
|
||||
return False
|
||||
return self.getheader("Location")
|
||||
|
||||
@property
|
||||
def data(self):
|
||||
return self._content.getbuffer().tobytes()
|
||||
|
||||
def drain_conn(self):
|
||||
pass
|
||||
|
||||
def stream(self, amt=65536, decode_content=None):
|
||||
while True:
|
||||
b = self._content.read(amt)
|
||||
yield b
|
||||
if not b:
|
||||
break
|
||||
|
||||
|
||||
class VCRConnection:
|
||||
# A reference to the cassette that's currently being patched in
|
||||
cassette = None
|
||||
|
||||
@@ -153,26 +188,26 @@ class VCRConnection(object):
|
||||
"""
|
||||
port = self.real_connection.port
|
||||
default_port = {"https": 443, "http": 80}[self._protocol]
|
||||
return ":{}".format(port) if port != default_port else ""
|
||||
return f":{port}" if port != default_port else ""
|
||||
|
||||
def _uri(self, url):
|
||||
"""Returns request absolute URI"""
|
||||
if url and not url.startswith("/"):
|
||||
# Then this must be a proxy request.
|
||||
return url
|
||||
uri = "{}://{}{}{}".format(self._protocol, self.real_connection.host, self._port_postfix(), url)
|
||||
uri = f"{self._protocol}://{self.real_connection.host}{self._port_postfix()}{url}"
|
||||
log.debug("Absolute URI: %s", uri)
|
||||
return uri
|
||||
|
||||
def _url(self, uri):
|
||||
"""Returns request selector url from absolute URI"""
|
||||
prefix = "{}://{}{}".format(self._protocol, self.real_connection.host, self._port_postfix())
|
||||
prefix = f"{self._protocol}://{self.real_connection.host}{self._port_postfix()}"
|
||||
return uri.replace(prefix, "", 1)
|
||||
|
||||
def request(self, method, url, body=None, headers=None, *args, **kwargs):
|
||||
"""Persist the request metadata in self._vcr_request"""
|
||||
self._vcr_request = Request(method=method, uri=self._uri(url), body=body, headers=headers or {})
|
||||
log.debug("Got {}".format(self._vcr_request))
|
||||
log.debug(f"Got {self._vcr_request}")
|
||||
|
||||
# Note: The request may not actually be finished at this point, so
|
||||
# I'm not sending the actual request until getresponse(). This
|
||||
@@ -188,7 +223,7 @@ class VCRConnection(object):
|
||||
of putheader() calls.
|
||||
"""
|
||||
self._vcr_request = Request(method=method, uri=self._uri(url), body="", headers={})
|
||||
log.debug("Got {}".format(self._vcr_request))
|
||||
log.debug(f"Got {self._vcr_request}")
|
||||
|
||||
def putheader(self, header, *values):
|
||||
self._vcr_request.headers[header] = values
|
||||
@@ -220,19 +255,20 @@ class VCRConnection(object):
|
||||
# Check to see if the cassette has a response for this request. If so,
|
||||
# then return it
|
||||
if self.cassette.can_play_response_for(self._vcr_request):
|
||||
log.info("Playing response for {} from cassette".format(self._vcr_request))
|
||||
log.info(f"Playing response for {self._vcr_request} from cassette")
|
||||
response = self.cassette.play_response(self._vcr_request)
|
||||
return VCRHTTPResponse(response)
|
||||
else:
|
||||
if self.cassette.write_protected and self.cassette.filter_request(self._vcr_request):
|
||||
raise CannotOverwriteExistingCassetteException(
|
||||
cassette=self.cassette, failed_request=self._vcr_request
|
||||
cassette=self.cassette,
|
||||
failed_request=self._vcr_request,
|
||||
)
|
||||
|
||||
# Otherwise, we should send the request, then get the response
|
||||
# and return it.
|
||||
|
||||
log.info("{} not in cassette, sending to real server".format(self._vcr_request))
|
||||
log.info(f"{self._vcr_request} not in cassette, sending to real server")
|
||||
# This is imported here to avoid circular import.
|
||||
# TODO(@IvanMalison): Refactor to allow normal import.
|
||||
from vcr.patch import force_reset
|
||||
@@ -247,12 +283,13 @@ class VCRConnection(object):
|
||||
|
||||
# get the response
|
||||
response = self.real_connection.getresponse()
|
||||
response_data = response.data if hasattr(response, "data") else response.read()
|
||||
|
||||
# put the response into the cassette
|
||||
response = {
|
||||
"status": {"code": response.status, "message": response.reason},
|
||||
"headers": serialize_headers(response),
|
||||
"body": {"string": response.read()},
|
||||
"body": {"string": response_data},
|
||||
}
|
||||
self.cassette.append(self._vcr_request, response)
|
||||
return VCRHTTPResponse(response)
|
||||
@@ -296,8 +333,7 @@ class VCRConnection(object):
|
||||
self.real_connection.sock = value
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
if six.PY3:
|
||||
kwargs.pop("strict", None) # apparently this is gone in py3
|
||||
kwargs.pop("strict", None) # apparently this is gone in py3
|
||||
|
||||
# need to temporarily reset here because the real connection
|
||||
# inherits from the thing that we are mocking out. Take out
|
||||
@@ -312,7 +348,7 @@ class VCRConnection(object):
|
||||
def __setattr__(self, name, value):
|
||||
"""
|
||||
We need to define this because any attributes that are set on the
|
||||
VCRConnection need to be propogated to the real connection.
|
||||
VCRConnection need to be propagated to the real connection.
|
||||
|
||||
For example, urllib3 will set certain attributes on the connection,
|
||||
such as 'ssl_version'. These attributes need to get set on the real
|
||||
@@ -328,7 +364,7 @@ class VCRConnection(object):
|
||||
# we're setting the real_connection itself for the first time
|
||||
pass
|
||||
|
||||
super(VCRConnection, self).__setattr__(name, value)
|
||||
super().__setattr__(name, value)
|
||||
|
||||
def __getattr__(self, name):
|
||||
"""
|
||||
@@ -340,7 +376,7 @@ class VCRConnection(object):
|
||||
# we're setting the real_connection itself for the first time
|
||||
return getattr(self.real_connection, name)
|
||||
|
||||
return super(VCRConnection, self).__getattr__(name)
|
||||
return super().__getattr__(name)
|
||||
|
||||
|
||||
for k, v in HTTPConnection.__dict__.items():
|
||||
|
||||
279
vcr/stubs/aiohttp_stubs.py
Normal file
279
vcr/stubs/aiohttp_stubs.py
Normal file
@@ -0,0 +1,279 @@
|
||||
"""Stubs for aiohttp HTTP clients"""
|
||||
import asyncio
|
||||
import functools
|
||||
import json
|
||||
import logging
|
||||
from http.cookies import CookieError, Morsel, SimpleCookie
|
||||
from typing import Mapping, Union
|
||||
|
||||
from aiohttp import ClientConnectionError, ClientResponse, CookieJar, RequestInfo, hdrs, streams
|
||||
from aiohttp.helpers import strip_auth_from_url
|
||||
from multidict import CIMultiDict, CIMultiDictProxy, MultiDict
|
||||
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):
|
||||
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_request.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, kwargs):
|
||||
history = []
|
||||
allow_redirects = kwargs.get("allow_redirects", True)
|
||||
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 allow_redirects and 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,
|
||||
}
|
||||
|
||||
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 i, past_response in enumerate(response.history):
|
||||
aiohttp_request = past_response.request_info
|
||||
|
||||
past_request = Request(
|
||||
aiohttp_request.method,
|
||||
str(aiohttp_request.url),
|
||||
# Record body of first request, rest are following a redirect.
|
||||
None if i else vcr_request.body,
|
||||
_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 _build_url_with_params(url_str: str, params: Mapping[str, Union[str, int, float]]) -> URL:
|
||||
# This code is basically a copy&paste of aiohttp.
|
||||
# https://github.com/aio-libs/aiohttp/blob/master/aiohttp/client_reqrep.py#L225
|
||||
url = URL(url_str)
|
||||
q = MultiDict(url.query)
|
||||
url2 = url.with_query(params)
|
||||
q.extend(url2.query)
|
||||
return url.with_query(q)
|
||||
|
||||
|
||||
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 not params else _build_url_with_params(url, 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(f"Playing response for {vcr_request} from cassette")
|
||||
response = play_responses(cassette, vcr_request, kwargs)
|
||||
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)
|
||||
await record_responses(cassette, vcr_request, response)
|
||||
return response
|
||||
|
||||
return new_request
|
||||
@@ -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
|
||||
@@ -1,19 +1,6 @@
|
||||
"""Stubs for boto3"""
|
||||
import six
|
||||
|
||||
try:
|
||||
# boto using awsrequest
|
||||
from botocore.awsrequest import AWSHTTPConnection as HTTPConnection
|
||||
from botocore.awsrequest import AWSHTTPSConnection as VerifiedHTTPSConnection
|
||||
|
||||
except ImportError: # pragma: nocover
|
||||
# boto using vendored requests
|
||||
# urllib3 defines its own HTTPConnection classes, which boto3 goes ahead and assumes
|
||||
# you're using. It includes some polyfills for newer features missing in older pythons.
|
||||
try:
|
||||
from urllib3.connectionpool import HTTPConnection, VerifiedHTTPSConnection
|
||||
except ImportError: # pragma: nocover
|
||||
from requests.packages.urllib3.connectionpool import HTTPConnection, VerifiedHTTPSConnection
|
||||
from botocore.awsrequest import AWSHTTPConnection as HTTPConnection
|
||||
from botocore.awsrequest import AWSHTTPSConnection as VerifiedHTTPSConnection
|
||||
|
||||
from ..stubs import VCRHTTPConnection, VCRHTTPSConnection
|
||||
|
||||
@@ -26,8 +13,7 @@ class VCRRequestsHTTPSConnection(VCRHTTPSConnection, VerifiedHTTPSConnection):
|
||||
_baseclass = VerifiedHTTPSConnection
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
if six.PY3:
|
||||
kwargs.pop("strict", None) # apparently this is gone in py3
|
||||
kwargs.pop("strict", None)
|
||||
|
||||
# need to temporarily reset here because the real connection
|
||||
# inherits from the thing that we are mocking out. Take out
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""Stubs for boto"""
|
||||
|
||||
from boto.https_connection import CertValidatingHTTPSConnection
|
||||
|
||||
from ..stubs import VCRHTTPSConnection
|
||||
|
||||
|
||||
|
||||
@@ -1,12 +1,5 @@
|
||||
import six
|
||||
from six import BytesIO
|
||||
from six.moves.http_client import HTTPMessage
|
||||
|
||||
try:
|
||||
import http.client
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
import http.client
|
||||
from io import BytesIO
|
||||
|
||||
"""
|
||||
The python3 http.client api moved some stuff around, so this is an abstraction
|
||||
@@ -15,30 +8,19 @@ layer that tries to cope with this move.
|
||||
|
||||
|
||||
def get_header(message, name):
|
||||
if six.PY3:
|
||||
return message.getallmatchingheaders(name)
|
||||
else:
|
||||
return message.getheader(name)
|
||||
return message.getallmatchingheaders(name)
|
||||
|
||||
|
||||
def get_header_items(message):
|
||||
for (key, values) in get_headers(message):
|
||||
for key, values in get_headers(message):
|
||||
for value in values:
|
||||
yield key, value
|
||||
|
||||
|
||||
def get_headers(message):
|
||||
for key in set(message.keys()):
|
||||
if six.PY3:
|
||||
yield key, message.get_all(key)
|
||||
else:
|
||||
yield key, message.getheaders(key)
|
||||
yield key, message.get_all(key)
|
||||
|
||||
|
||||
def get_httpmessage(headers):
|
||||
if six.PY3:
|
||||
return http.client.parse_headers(BytesIO(headers))
|
||||
msg = HTTPMessage(BytesIO(headers))
|
||||
msg.fp.seek(0)
|
||||
msg.readheaders()
|
||||
return msg
|
||||
return http.client.parse_headers(BytesIO(headers))
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""Stubs for httplib2"""
|
||||
|
||||
from httplib2 import HTTPConnectionWithTimeout, HTTPSConnectionWithTimeout
|
||||
|
||||
from ..stubs import VCRHTTPConnection, VCRHTTPSConnection
|
||||
|
||||
|
||||
@@ -27,7 +28,6 @@ class VCRHTTPSConnectionWithTimeout(VCRHTTPSConnection, HTTPSConnectionWithTimeo
|
||||
_baseclass = HTTPSConnectionWithTimeout
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
||||
# Delete the keyword arguments that HTTPSConnection would not recognize
|
||||
safe_keys = {
|
||||
"host",
|
||||
|
||||
171
vcr/stubs/httpx_stubs.py
Normal file
171
vcr/stubs/httpx_stubs.py
Normal file
@@ -0,0 +1,171 @@
|
||||
import functools
|
||||
import inspect
|
||||
import logging
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import httpx
|
||||
|
||||
from vcr.errors import CannotOverwriteExistingCassetteException
|
||||
from vcr.request import Request as VcrRequest
|
||||
|
||||
_httpx_signature = inspect.signature(httpx.Client.request)
|
||||
|
||||
try:
|
||||
HTTPX_REDIRECT_PARAM = _httpx_signature.parameters["follow_redirects"]
|
||||
except KeyError:
|
||||
HTTPX_REDIRECT_PARAM = _httpx_signature.parameters["allow_redirects"]
|
||||
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _transform_headers(httpx_response):
|
||||
"""
|
||||
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_response.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_response):
|
||||
return {
|
||||
"status_code": httpx_response.status_code,
|
||||
"http_version": httpx_response.http_version,
|
||||
"headers": _transform_headers(httpx_response),
|
||||
"content": httpx_response.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(
|
||||
HTTPX_REDIRECT_PARAM.name,
|
||||
HTTPX_REDIRECT_PARAM.default,
|
||||
)
|
||||
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
|
||||
@@ -1,9 +1,6 @@
|
||||
"""Stubs for requests"""
|
||||
|
||||
try:
|
||||
from urllib3.connectionpool import HTTPConnection, VerifiedHTTPSConnection
|
||||
except ImportError:
|
||||
from requests.packages.urllib3.connectionpool import HTTPConnection, VerifiedHTTPSConnection
|
||||
from urllib3.connection import HTTPConnection, VerifiedHTTPSConnection
|
||||
|
||||
from ..stubs import VCRHTTPConnection, VCRHTTPSConnection
|
||||
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
"""Stubs for tornado HTTP clients"""
|
||||
from __future__ import absolute_import
|
||||
|
||||
import functools
|
||||
from six import BytesIO
|
||||
from io import BytesIO
|
||||
|
||||
from tornado import httputil
|
||||
from tornado.httpclient import HTTPResponse
|
||||
@@ -31,9 +29,9 @@ def vcr_fetch_impl(cassette, real_fetch_impl):
|
||||
request,
|
||||
599,
|
||||
error=Exception(
|
||||
"The request (%s) uses AsyncHTTPClient functionality "
|
||||
f"The request ({request!r}) uses AsyncHTTPClient functionality "
|
||||
"that is not yet supported by VCR.py. Please make the "
|
||||
"request outside a VCR.py context." % repr(request)
|
||||
"request outside a VCR.py context.",
|
||||
),
|
||||
request_time=self.io_loop.time() - request.start_time,
|
||||
)
|
||||
@@ -67,7 +65,8 @@ def vcr_fetch_impl(cassette, real_fetch_impl):
|
||||
request,
|
||||
599,
|
||||
error=CannotOverwriteExistingCassetteException(
|
||||
cassette=cassette, failed_request=vcr_request
|
||||
cassette=cassette,
|
||||
failed_request=vcr_request,
|
||||
),
|
||||
request_time=self.io_loop.time() - request.start_time,
|
||||
)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""Stubs for urllib3"""
|
||||
|
||||
from urllib3.connectionpool import HTTPConnection, VerifiedHTTPSConnection
|
||||
from urllib3.connection import HTTPConnection, VerifiedHTTPSConnection
|
||||
|
||||
from ..stubs import VCRHTTPConnection, VCRHTTPSConnection
|
||||
|
||||
# urllib3 defines its own HTTPConnection classes. It includes some polyfills
|
||||
|
||||
39
vcr/unittest.py
Normal file
39
vcr/unittest.py
Normal file
@@ -0,0 +1,39 @@
|
||||
import inspect
|
||||
import os
|
||||
import unittest
|
||||
|
||||
from .config import VCR
|
||||
|
||||
|
||||
class VCRMixin:
|
||||
"""A TestCase mixin that provides VCR integration."""
|
||||
|
||||
vcr_enabled = True
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
if self.vcr_enabled:
|
||||
kwargs = self._get_vcr_kwargs()
|
||||
myvcr = self._get_vcr(**kwargs)
|
||||
cm = myvcr.use_cassette(self._get_cassette_name())
|
||||
self.cassette = cm.__enter__()
|
||||
self.addCleanup(cm.__exit__, None, None, None)
|
||||
|
||||
def _get_vcr(self, **kwargs):
|
||||
if "cassette_library_dir" not in kwargs:
|
||||
kwargs["cassette_library_dir"] = self._get_cassette_library_dir()
|
||||
return VCR(**kwargs)
|
||||
|
||||
def _get_vcr_kwargs(self, **kwargs):
|
||||
return kwargs
|
||||
|
||||
def _get_cassette_library_dir(self):
|
||||
testdir = os.path.dirname(inspect.getfile(self.__class__))
|
||||
return os.path.join(testdir, "cassettes")
|
||||
|
||||
def _get_cassette_name(self):
|
||||
return f"{self.__class__.__name__}.{self._testMethodName}.yaml"
|
||||
|
||||
|
||||
class VCRTestCase(VCRMixin, unittest.TestCase):
|
||||
pass
|
||||
12
vcr/util.py
12
vcr/util.py
@@ -1,9 +1,5 @@
|
||||
import types
|
||||
|
||||
try:
|
||||
from collections.abc import Mapping, MutableMapping
|
||||
except ImportError:
|
||||
from collections import Mapping, MutableMapping
|
||||
from collections.abc import Mapping, MutableMapping
|
||||
|
||||
|
||||
# Shamelessly stolen from https://github.com/kennethreitz/requests/blob/master/requests/structures.py
|
||||
@@ -31,7 +27,7 @@ class CaseInsensitiveDict(MutableMapping):
|
||||
"""
|
||||
|
||||
def __init__(self, data=None, **kwargs):
|
||||
self._store = dict()
|
||||
self._store = {}
|
||||
if data is None:
|
||||
data = {}
|
||||
self.update(data, **kwargs)
|
||||
@@ -107,12 +103,12 @@ def auto_decorate(decorator, predicate=lambda name, value: isinstance(value, typ
|
||||
|
||||
class DecorateAll(type):
|
||||
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):
|
||||
new_attributes_dict = {
|
||||
attribute: maybe_decorate(attribute, value) for attribute, value in attributes_dict.items()
|
||||
}
|
||||
return super(DecorateAll, cls).__new__(cls, name, bases, new_attributes_dict)
|
||||
return super().__new__(cls, name, bases, new_attributes_dict)
|
||||
|
||||
return DecorateAll
|
||||
|
||||
Reference in New Issue
Block a user