mirror of
https://github.com/kevin1024/vcrpy.git
synced 2025-12-08 16:53:23 +00:00
Compare commits
85 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9c46831a8e | ||
|
|
fe596447ec | ||
|
|
be1035fd5d | ||
|
|
eb96c590ff | ||
|
|
7add8c0bab | ||
|
|
b1bc5c3a02 | ||
|
|
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 |
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
|
||||
40
.github/workflows/main.yml
vendored
Normal file
40
.github/workflows/main.yml
vendored
Normal file
@@ -0,0 +1,40 @@
|
||||
name: Test
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
branches:
|
||||
- "*"
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["3.7", "3.8", "3.9", "3.10", "pypy-3.8"]
|
||||
|
||||
steps:
|
||||
- name: Install libgnutls28-dev
|
||||
run: |
|
||||
sudo apt update -q
|
||||
sudo apt install -q -y libgnutls28-dev libcurl4-gnutls-dev
|
||||
|
||||
- uses: actions/checkout@v3.0.2
|
||||
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
- name: Install project dependencies
|
||||
run: |
|
||||
pip install --upgrade pip
|
||||
pip install codecov tox tox-gh-actions
|
||||
|
||||
- name: Run tests with tox
|
||||
run: tox
|
||||
|
||||
- name: Run coverage
|
||||
run: codecov
|
||||
32
.travis.yml
32
.travis.yml
@@ -1,32 +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"
|
||||
python:
|
||||
- "3.5"
|
||||
- "3.6"
|
||||
- "3.7"
|
||||
- "3.8"
|
||||
- "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
|
||||
@@ -47,6 +47,11 @@ 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
|
||||
-------
|
||||
|
||||
@@ -57,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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -269,7 +271,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
|
||||
|
||||
@@ -386,3 +388,19 @@ VCR.py allows to rewind a cassette in order to replay it inside the same functio
|
||||
assert cass.all_played
|
||||
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
|
||||
|
||||
@@ -1,14 +1,31 @@
|
||||
Changelog
|
||||
---------
|
||||
|
||||
For a full list of triaged issues, bugs and PRs and what release they are targetted for please see the following link.
|
||||
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.
|
||||
|
||||
- UNRELEASED
|
||||
- ...
|
||||
- 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
|
||||
@@ -92,7 +109,7 @@ All help in providing PRs to close out bug issues is appreciated. Even if that i
|
||||
- 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.
|
||||
- [#199] Fix path transformation defaults.
|
||||
- Better headers dictionary management.
|
||||
- 1.7.3 [#188]
|
||||
- ``additional_matchers`` kwarg on ``use_cassette``.
|
||||
@@ -195,7 +212,7 @@ All help in providing PRs to close out bug issues is appreciated. Even if that i
|
||||
- 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
|
||||
- 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
|
||||
|
||||
@@ -96,11 +96,11 @@ The test suite is pretty big and slow, but you can tell tox to only run specific
|
||||
|
||||
tox -e {pyNN}-{HTTP_LIBRARY} -- <pytest flags passed through>
|
||||
|
||||
tox -e py36-requests -- -v -k "'test_status_code or test_gzip'"
|
||||
tox -e py37-requests -- -v -k "'test_status_code or test_gzip'"
|
||||
tox -e py37-requests -- -v --last-failed
|
||||
|
||||
This will run only tests that look like ``test_status_code`` or
|
||||
``test_gzip`` in the test suite, and only in the python 3.6 environment
|
||||
``test_gzip`` in the test suite, and only in the python 3.7 environment
|
||||
that has ``requests`` installed.
|
||||
|
||||
Also, in order for the boto tests to run, you will need an AWS key.
|
||||
@@ -130,10 +130,10 @@ in this example::
|
||||
pip install tox tox-pyenv
|
||||
|
||||
# Install supported versions (at time of writing), this does not activate them
|
||||
pyenv install 3.5.9 3.6.9 3.7.5 3.8.0 pypy3.6-7.2.0
|
||||
pyenv install 3.7.5 3.8.0 pypy3.8
|
||||
|
||||
# This activates them
|
||||
pyenv local 3.5.9 3.6.9 3.7.5 3.8.0 pypy3.6-7.2.0
|
||||
pyenv local 3.7.5 3.8.0 pypy3.8
|
||||
|
||||
# Run the whole test suite
|
||||
tox
|
||||
|
||||
@@ -9,7 +9,7 @@ with pip::
|
||||
Compatibility
|
||||
-------------
|
||||
|
||||
VCR.py supports Python 3.5+, and `pypy <http://pypy.org>`__.
|
||||
VCR.py supports Python 3.7+, and `pypy <http://pypy.org>`__.
|
||||
|
||||
The following HTTP libraries are supported:
|
||||
|
||||
@@ -22,6 +22,7 @@ The following HTTP libraries are supported:
|
||||
- ``tornado.httpclient``
|
||||
- ``urllib2``
|
||||
- ``urllib3``
|
||||
- ``httpx``
|
||||
|
||||
Speed
|
||||
-----
|
||||
|
||||
@@ -4,10 +4,10 @@ Usage
|
||||
.. code:: python
|
||||
|
||||
import vcr
|
||||
import urllib2
|
||||
import urllib
|
||||
|
||||
with vcr.use_cassette('fixtures/vcr_cassettes/synopsis.yaml'):
|
||||
response = urllib2.urlopen('http://www.iana.org/domains/reserved').read()
|
||||
response = urllib.request.urlopen('http://www.iana.org/domains/reserved').read()
|
||||
assert 'Example domains' in response
|
||||
|
||||
Run this test once, and VCR.py will record the HTTP request to
|
||||
@@ -25,7 +25,7 @@ look like this:
|
||||
|
||||
@vcr.use_cassette('fixtures/vcr_cassettes/synopsis.yaml')
|
||||
def test_iana():
|
||||
response = urllib2.urlopen('http://www.iana.org/domains/reserved').read()
|
||||
response = urllib.request.urlopen('http://www.iana.org/domains/reserved').read()
|
||||
assert 'Example domains' in response
|
||||
|
||||
When using the decorator version of ``use_cassette``, it is possible to
|
||||
@@ -35,7 +35,7 @@ omit the path to the cassette file.
|
||||
|
||||
@vcr.use_cassette()
|
||||
def test_iana():
|
||||
response = urllib2.urlopen('http://www.iana.org/domains/reserved').read()
|
||||
response = urllib.request.urlopen('http://www.iana.org/domains/reserved').read()
|
||||
assert 'Example domains' in response
|
||||
|
||||
In this case, the cassette file will be given the same name as the test
|
||||
|
||||
@@ -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=`python -m pytest_httpbin.certs` pytest $*
|
||||
|
||||
9
setup.py
9
setup.py
@@ -46,8 +46,7 @@ install_requires = [
|
||||
"PyYAML",
|
||||
"wrapt",
|
||||
"six>=1.5",
|
||||
'yarl; python_version>="3.6"',
|
||||
'yarl<1.4; python_version=="3.5"',
|
||||
"yarl",
|
||||
]
|
||||
|
||||
setup(
|
||||
@@ -60,7 +59,7 @@ setup(
|
||||
author_email="me@kevinmccarthy.org",
|
||||
url="https://github.com/kevin1024/vcrpy",
|
||||
packages=find_packages(exclude=["tests*"]),
|
||||
python_requires=">=3.5",
|
||||
python_requires=">=3.7",
|
||||
install_requires=install_requires,
|
||||
license="MIT",
|
||||
tests_require=["pytest", "mock", "pytest-httpbin"],
|
||||
@@ -70,10 +69,10 @@ setup(
|
||||
"Intended Audience :: Developers",
|
||||
"Programming Language :: Python",
|
||||
"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 :: Only",
|
||||
"Programming Language :: Python :: Implementation :: CPython",
|
||||
"Programming Language :: Python :: Implementation :: PyPy",
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
interactions:
|
||||
- request:
|
||||
body: ''
|
||||
headers:
|
||||
accept:
|
||||
- '*/*'
|
||||
accept-encoding:
|
||||
- gzip, deflate, br
|
||||
connection:
|
||||
- keep-alive
|
||||
host:
|
||||
- httpbin.org
|
||||
user-agent:
|
||||
- python-httpx/0.12.1
|
||||
method: GET
|
||||
uri: https://httpbin.org/headers
|
||||
response:
|
||||
content: "{\n \"headers\": {\n \"Accept\": \"*/*\", \n \"Accept-Encoding\"\
|
||||
: \"gzip, deflate, br\", \n \"Host\": \"httpbin.org\", \n \"User-Agent\"\
|
||||
: \"python-httpx/0.12.1\", \n \"X-Amzn-Trace-Id\": \"Root=1-5ea778c9-ea76170da792abdbf7614067\"\
|
||||
\n }\n}\n"
|
||||
headers:
|
||||
access-control-allow-credentials:
|
||||
- 'true'
|
||||
access-control-allow-origin:
|
||||
- '*'
|
||||
connection:
|
||||
- keep-alive
|
||||
content-length:
|
||||
- '226'
|
||||
content-type:
|
||||
- application/json
|
||||
date:
|
||||
- Tue, 28 Apr 2020 00:28:57 GMT
|
||||
server:
|
||||
- gunicorn/19.9.0
|
||||
via:
|
||||
- my_own_proxy
|
||||
http_version: HTTP/1.1
|
||||
status_code: 200
|
||||
version: 1
|
||||
@@ -1,5 +1,6 @@
|
||||
import contextlib
|
||||
import logging
|
||||
import urllib.parse
|
||||
|
||||
import pytest
|
||||
|
||||
@@ -151,12 +152,13 @@ def test_post(tmpdir, scheme, body, caplog):
|
||||
|
||||
|
||||
def test_params(tmpdir, scheme):
|
||||
url = scheme + "://httpbin.org/get"
|
||||
url = scheme + "://httpbin.org/get?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["args"] == {"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)
|
||||
@@ -167,7 +169,7 @@ def test_params(tmpdir, scheme):
|
||||
def test_params_same_url_distinct_params(tmpdir, scheme):
|
||||
url = scheme + "://httpbin.org/get"
|
||||
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,9 +181,8 @@ 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):
|
||||
@@ -250,7 +251,7 @@ def test_aiohttp_test_client_json(aiohttp_client, tmpdir):
|
||||
|
||||
|
||||
def test_redirect(aiohttp_client, tmpdir):
|
||||
url = "https://httpbin.org/redirect/2"
|
||||
url = "https://mockbin.org/redirect/302/2"
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join("redirect.yaml"))):
|
||||
response, _ = get(url)
|
||||
@@ -273,11 +274,28 @@ def test_redirect(aiohttp_client, tmpdir):
|
||||
assert cassette_response.request_info.real_url == response.request_info.real_url
|
||||
|
||||
|
||||
def test_not_modified(aiohttp_client, tmpdir):
|
||||
"""It doesn't try to redirect on 304"""
|
||||
url = "https://httpbin.org/status/304"
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join("not_modified.yaml"))):
|
||||
response, _ = get(url)
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join("not_modified.yaml"))) as cassette:
|
||||
cassette_response, _ = get(url)
|
||||
|
||||
assert cassette_response.status == 304
|
||||
assert response.status == 304
|
||||
assert len(cassette_response.history) == len(response.history)
|
||||
assert len(cassette) == 1
|
||||
assert cassette.play_count == 1
|
||||
|
||||
|
||||
def test_double_requests(tmpdir):
|
||||
"""We should capture, record, and replay all requests and response chains,
|
||||
even if there are duplicate ones.
|
||||
even if there are duplicate ones.
|
||||
|
||||
We should replay in the order we saw them.
|
||||
We should replay in the order we saw them.
|
||||
"""
|
||||
url = "https://httpbin.org/get"
|
||||
|
||||
@@ -302,3 +320,99 @@ def test_double_requests(tmpdir):
|
||||
|
||||
# Now that we made both requests, we should have played both.
|
||||
assert cassette.play_count == 2
|
||||
|
||||
|
||||
def test_cookies(scheme, tmpdir):
|
||||
async def run(loop):
|
||||
cookies_url = scheme + (
|
||||
"://httpbin.org/response-headers?"
|
||||
"set-cookie=" + urllib.parse.quote("cookie_1=val_1; Path=/") + "&"
|
||||
"Set-Cookie=" + urllib.parse.quote("Cookie_2=Val_2; Path=/")
|
||||
)
|
||||
home_url = scheme + "://httpbin.org/"
|
||||
tmp = str(tmpdir.join("cookies.yaml"))
|
||||
req_cookies = {"Cookie_3": "Val_3"}
|
||||
req_headers = {"Cookie": "Cookie_4=Val_4"}
|
||||
|
||||
# ------------------------- Record -------------------------- #
|
||||
with vcr.use_cassette(tmp) as cassette:
|
||||
async with aiohttp.ClientSession(loop=loop) as session:
|
||||
cookies_resp = await session.get(cookies_url)
|
||||
home_resp = await session.get(home_url, cookies=req_cookies, headers=req_headers)
|
||||
assert cassette.play_count == 0
|
||||
assert_responses(cookies_resp, home_resp)
|
||||
|
||||
# -------------------------- Play --------------------------- #
|
||||
with vcr.use_cassette(tmp, record_mode=vcr.mode.NONE) as cassette:
|
||||
async with aiohttp.ClientSession(loop=loop) as session:
|
||||
cookies_resp = await session.get(cookies_url)
|
||||
home_resp = await session.get(home_url, cookies=req_cookies, headers=req_headers)
|
||||
assert cassette.play_count == 2
|
||||
assert_responses(cookies_resp, home_resp)
|
||||
|
||||
def assert_responses(cookies_resp, home_resp):
|
||||
assert cookies_resp.cookies.get("cookie_1").value == "val_1"
|
||||
assert cookies_resp.cookies.get("Cookie_2").value == "Val_2"
|
||||
request_cookies = home_resp.request_info.headers["cookie"]
|
||||
assert "cookie_1=val_1" in request_cookies
|
||||
assert "Cookie_2=Val_2" in request_cookies
|
||||
assert "Cookie_3=Val_3" in request_cookies
|
||||
assert "Cookie_4=Val_4" in request_cookies
|
||||
|
||||
run_in_loop(run)
|
||||
|
||||
|
||||
def test_cookies_redirect(scheme, tmpdir):
|
||||
async def run(loop):
|
||||
# Sets cookie as provided by the query string and redirects
|
||||
cookies_url = scheme + "://httpbin.org/cookies/set?Cookie_1=Val_1"
|
||||
tmp = str(tmpdir.join("cookies.yaml"))
|
||||
|
||||
# ------------------------- Record -------------------------- #
|
||||
with vcr.use_cassette(tmp) as cassette:
|
||||
async with aiohttp.ClientSession(loop=loop) as session:
|
||||
cookies_resp = await session.get(cookies_url)
|
||||
assert not cookies_resp.cookies
|
||||
cookies = session.cookie_jar.filter_cookies(cookies_url)
|
||||
assert cookies["Cookie_1"].value == "Val_1"
|
||||
assert cassette.play_count == 0
|
||||
cassette.requests[1].headers["Cookie"] == "Cookie_1=Val_1"
|
||||
|
||||
# -------------------------- Play --------------------------- #
|
||||
with vcr.use_cassette(tmp, record_mode=vcr.mode.NONE) as cassette:
|
||||
async with aiohttp.ClientSession(loop=loop) as session:
|
||||
cookies_resp = await session.get(cookies_url)
|
||||
assert not cookies_resp.cookies
|
||||
cookies = session.cookie_jar.filter_cookies(cookies_url)
|
||||
assert cookies["Cookie_1"].value == "Val_1"
|
||||
assert cassette.play_count == 2
|
||||
cassette.requests[1].headers["Cookie"] == "Cookie_1=Val_1"
|
||||
|
||||
# Assert that it's ignoring expiration date
|
||||
with vcr.use_cassette(tmp, record_mode=vcr.mode.NONE) as cassette:
|
||||
cassette.responses[0]["headers"]["set-cookie"] = [
|
||||
"Cookie_1=Val_1; Expires=Wed, 21 Oct 2015 07:28:00 GMT"
|
||||
]
|
||||
async with aiohttp.ClientSession(loop=loop) as session:
|
||||
cookies_resp = await session.get(cookies_url)
|
||||
assert not cookies_resp.cookies
|
||||
cookies = session.cookie_jar.filter_cookies(cookies_url)
|
||||
assert cookies["Cookie_1"].value == "Val_1"
|
||||
|
||||
run_in_loop(run)
|
||||
|
||||
|
||||
def test_not_allow_redirects(tmpdir):
|
||||
url = "https://mockbin.org/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
|
||||
|
||||
@@ -11,7 +11,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
|
||||
|
||||
@@ -13,7 +13,9 @@ def test_set_serializer_default_config(tmpdir, httpbin):
|
||||
urlopen(httpbin.url + "/get")
|
||||
|
||||
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):
|
||||
|
||||
@@ -45,7 +45,7 @@ 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:
|
||||
with vcr.use_cassette(fname, record_mode=vcr.mode.ANY) as cass:
|
||||
urlopen(httpbin.url).read()
|
||||
urlopen(httpbin.url + "/get").read()
|
||||
assert cass.play_count == 1
|
||||
|
||||
@@ -61,13 +61,14 @@ def test_response_headers(tmpdir, httpbin_both):
|
||||
assert set(headers) == set(resp.items())
|
||||
|
||||
|
||||
def test_effective_url(tmpdir, httpbin_both):
|
||||
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)
|
||||
|
||||
299
tests/integration/test_httpx.py
Normal file
299
tests/integration/test_httpx.py
Normal file
@@ -0,0 +1,299 @@
|
||||
import pytest
|
||||
import os
|
||||
|
||||
asyncio = pytest.importorskip("asyncio")
|
||||
httpx = pytest.importorskip("httpx")
|
||||
|
||||
import vcr # noqa: E402
|
||||
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:
|
||||
raise ValueError('To access async client, use "with do_request() as client"')
|
||||
|
||||
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])
|
||||
if "scheme" in metafunc.fixturenames:
|
||||
metafunc.parametrize("scheme", ["http", "https"])
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def yml(tmpdir, request):
|
||||
return str(tmpdir.join(request.function.__name__ + ".yaml"))
|
||||
|
||||
|
||||
def test_status(tmpdir, scheme, do_request):
|
||||
url = scheme + "://mockbin.org/request"
|
||||
with vcr.use_cassette(str(tmpdir.join("status.yaml"))):
|
||||
response = do_request()("GET", url)
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join("status.yaml"))) as cassette:
|
||||
cassette_response = do_request()("GET", url)
|
||||
assert cassette_response.status_code == response.status_code
|
||||
assert cassette.play_count == 1
|
||||
|
||||
|
||||
def test_case_insensitive_headers(tmpdir, scheme, do_request):
|
||||
url = scheme + "://mockbin.org/request"
|
||||
with vcr.use_cassette(str(tmpdir.join("whatever.yaml"))):
|
||||
do_request()("GET", url)
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join("whatever.yaml"))) as cassette:
|
||||
cassette_response = do_request()("GET", url)
|
||||
assert "Content-Type" in cassette_response.headers
|
||||
assert "content-type" in cassette_response.headers
|
||||
assert cassette.play_count == 1
|
||||
|
||||
|
||||
def test_content(tmpdir, scheme, do_request):
|
||||
url = scheme + "://httpbin.org"
|
||||
with vcr.use_cassette(str(tmpdir.join("cointent.yaml"))):
|
||||
response = do_request()("GET", url)
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join("cointent.yaml"))) as cassette:
|
||||
cassette_response = do_request()("GET", url)
|
||||
assert cassette_response.content == response.content
|
||||
assert cassette.play_count == 1
|
||||
|
||||
|
||||
def test_json(tmpdir, scheme, do_request):
|
||||
url = scheme + "://httpbin.org/get"
|
||||
headers = {"Content-Type": "application/json"}
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join("json.yaml"))):
|
||||
response = do_request(headers=headers)("GET", url)
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join("json.yaml"))) as cassette:
|
||||
cassette_response = do_request(headers=headers)("GET", url)
|
||||
assert cassette_response.json() == response.json()
|
||||
assert cassette.play_count == 1
|
||||
|
||||
|
||||
def test_params_same_url_distinct_params(tmpdir, scheme, do_request):
|
||||
url = scheme + "://httpbin.org/get"
|
||||
headers = {"Content-Type": "application/json"}
|
||||
params = {"a": 1, "b": False, "c": "c"}
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join("get.yaml"))) as cassette:
|
||||
response = do_request()("GET", url, params=params, headers=headers)
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join("get.yaml"))) as cassette:
|
||||
cassette_response = do_request()("GET", url, params=params, headers=headers)
|
||||
assert cassette_response.request.url == response.request.url
|
||||
assert cassette_response.json() == response.json()
|
||||
assert cassette.play_count == 1
|
||||
|
||||
params = {"other": "params"}
|
||||
with vcr.use_cassette(str(tmpdir.join("get.yaml"))) as cassette:
|
||||
with pytest.raises(vcr.errors.CannotOverwriteExistingCassetteException):
|
||||
do_request()("GET", url, params=params, headers=headers)
|
||||
|
||||
|
||||
def test_redirect(tmpdir, do_request, yml):
|
||||
url = "https://mockbin.org/redirect/303/2"
|
||||
|
||||
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 {k: v for k, v in cassette_response.request.headers.items()} == {
|
||||
k: v for k, v in response.request.headers.items()
|
||||
}
|
||||
|
||||
|
||||
def test_work_with_gzipped_data(tmpdir, do_request, yml):
|
||||
with vcr.use_cassette(yml):
|
||||
do_request()("GET", "https://httpbin.org/gzip")
|
||||
|
||||
with vcr.use_cassette(yml) as cassette:
|
||||
cassette_response = do_request()("GET", "https://httpbin.org/gzip")
|
||||
|
||||
assert "gzip" in cassette_response.json()["headers"]["Accept-Encoding"]
|
||||
assert cassette_response.read()
|
||||
assert cassette.play_count == 1
|
||||
|
||||
|
||||
@pytest.mark.parametrize("url", ["https://github.com/kevin1024/vcrpy/issues/" + str(i) for i in range(3, 6)])
|
||||
def test_simple_fetching(tmpdir, do_request, yml, url):
|
||||
with vcr.use_cassette(yml):
|
||||
do_request()("GET", url)
|
||||
|
||||
with vcr.use_cassette(yml) as cassette:
|
||||
cassette_response = do_request()("GET", url)
|
||||
assert str(cassette_response.request.url) == url
|
||||
assert cassette.play_count == 1
|
||||
|
||||
|
||||
def test_behind_proxy(do_request):
|
||||
# This is recorded because otherwise we should have a live proxy somewhere.
|
||||
yml = (
|
||||
os.path.dirname(os.path.realpath(__file__)) + "/cassettes/" + "test_httpx_test_test_behind_proxy.yml"
|
||||
)
|
||||
url = "https://httpbin.org/headers"
|
||||
proxy = "http://localhost:8080"
|
||||
proxies = {"http://": proxy, "https://": proxy}
|
||||
|
||||
with vcr.use_cassette(yml):
|
||||
response = do_request(proxies=proxies, verify=False)("GET", url)
|
||||
|
||||
with vcr.use_cassette(yml) as cassette:
|
||||
cassette_response = do_request(proxies=proxies, verify=False)("GET", url)
|
||||
assert str(cassette_response.request.url) == url
|
||||
assert cassette.play_count == 1
|
||||
|
||||
assert cassette_response.headers["Via"] == "my_own_proxy", str(cassette_response.headers)
|
||||
assert cassette_response.request.url == response.request.url
|
||||
|
||||
|
||||
def test_cookies(tmpdir, scheme, do_request):
|
||||
def client_cookies(client):
|
||||
return [c for c in client.client.cookies]
|
||||
|
||||
def response_cookies(response):
|
||||
return [c for c in response.cookies]
|
||||
|
||||
with do_request() as client:
|
||||
assert client_cookies(client) == []
|
||||
|
||||
redirect_kwargs = {HTTPX_REDIRECT_PARAM.name: True}
|
||||
|
||||
url = scheme + "://httpbin.org"
|
||||
testfile = str(tmpdir.join("cookies.yml"))
|
||||
with vcr.use_cassette(testfile):
|
||||
r1 = client("GET", url + "/cookies/set?k1=v1&k2=v2", **redirect_kwargs)
|
||||
assert response_cookies(r1.history[0]) == ["k1", "k2"]
|
||||
assert response_cookies(r1) == []
|
||||
|
||||
r2 = client("GET", url + "/cookies", **redirect_kwargs)
|
||||
assert len(r2.json()["cookies"]) == 2
|
||||
|
||||
assert client_cookies(client) == ["k1", "k2"]
|
||||
|
||||
with do_request() as new_client:
|
||||
assert client_cookies(new_client) == []
|
||||
|
||||
with vcr.use_cassette(testfile) as cassette:
|
||||
cassette_response = new_client("GET", url + "/cookies/set?k1=v1&k2=v2")
|
||||
assert response_cookies(cassette_response.history[0]) == ["k1", "k2"]
|
||||
assert response_cookies(cassette_response) == []
|
||||
|
||||
assert cassette.play_count == 2
|
||||
assert client_cookies(new_client) == ["k1", "k2"]
|
||||
|
||||
|
||||
def test_relative_redirects(tmpdir, scheme, do_request):
|
||||
redirect_kwargs = {HTTPX_REDIRECT_PARAM.name: True}
|
||||
|
||||
url = scheme + "://mockbin.com/redirect/301?to=/redirect/301?to=/request"
|
||||
testfile = str(tmpdir.join("relative_redirects.yml"))
|
||||
with vcr.use_cassette(testfile):
|
||||
response = do_request()("GET", url, **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
|
||||
|
||||
|
||||
def test_redirect_wo_allow_redirects(do_request, yml):
|
||||
url = "https://mockbin.org/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
|
||||
@@ -13,13 +13,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
|
||||
|
||||
|
||||
@@ -5,11 +5,11 @@ from urllib.request import urlopen
|
||||
|
||||
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()
|
||||
|
||||
@@ -22,12 +22,12 @@ def test_once_record_mode(tmpdir, httpbin):
|
||||
|
||||
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 +35,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 +45,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()
|
||||
|
||||
@@ -66,7 +66,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 +74,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,7 +86,7 @@ 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
|
||||
@@ -99,11 +99,11 @@ def test_new_episodes_record_mode_two_times(tmpdir, httpbin):
|
||||
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()
|
||||
|
||||
@@ -121,7 +121,7 @@ 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 vcr.use_cassette(testfile, record_mode=vcr.mode.NONE):
|
||||
with pytest.raises(Exception):
|
||||
urlopen(httpbin.url).read()
|
||||
|
||||
@@ -130,11 +130,11 @@ 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.
|
||||
|
||||
@@ -12,7 +12,7 @@ from vcr.persisters.filesystem import FilesystemPersister
|
||||
|
||||
class CustomFilesystemPersister(object):
|
||||
"""Behaves just like default FilesystemPersister but adds .test extension
|
||||
to the cassette file"""
|
||||
to the cassette file"""
|
||||
|
||||
@staticmethod
|
||||
def load_cassette(cassette_path, serializer):
|
||||
@@ -30,7 +30,7 @@ def test_save_cassette_with_custom_persister(tmpdir, httpbin):
|
||||
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
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Test requests' interaction with vcr"""
|
||||
import platform
|
||||
import pytest
|
||||
import sys
|
||||
import vcr
|
||||
from assertions import assert_cassette_empty, assert_is_json
|
||||
|
||||
@@ -117,10 +114,6 @@ def test_post_chunked_binary(tmpdir, httpbin):
|
||||
|
||||
|
||||
@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"])
|
||||
|
||||
@@ -90,10 +90,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 +156,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
|
||||
|
||||
|
||||
@@ -56,12 +56,13 @@ def test_response_headers(httpbin_both, tmpdir):
|
||||
assert sorted(open1) == sorted(open2)
|
||||
|
||||
|
||||
def test_effective_url(httpbin_both, tmpdir):
|
||||
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()
|
||||
|
||||
@@ -94,9 +94,10 @@ def test_post(tmpdir, httpbin_both, verify_pool_mgr):
|
||||
assert req1 == req2
|
||||
|
||||
|
||||
def test_redirects(tmpdir, httpbin_both, verify_pool_mgr):
|
||||
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 +105,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):
|
||||
|
||||
@@ -137,6 +137,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 x in range(10):
|
||||
assert a.play_response("foo") == "bar"
|
||||
assert a.play_count == 10
|
||||
assert a.all_played is False
|
||||
assert a.play_response("other") == "resp"
|
||||
assert a.play_count == 11
|
||||
assert a.all_played
|
||||
|
||||
a.allow_playback_repeats = False
|
||||
with pytest.raises(UnhandledHTTPRequestError) as e:
|
||||
a.play_response("foo")
|
||||
assert str(e.value) == "\"The cassette ('test') doesn't contain the request ('foo') asked for\""
|
||||
a.rewind()
|
||||
assert a.all_played is False
|
||||
assert a.play_response("foo") == "bar"
|
||||
assert a.all_played is False
|
||||
assert a.play_response("other") == "resp"
|
||||
assert a.all_played
|
||||
|
||||
|
||||
@mock.patch("vcr.cassette.requests_match", _mock_requests_match)
|
||||
def test_cassette_rewound():
|
||||
a = Cassette("test")
|
||||
@@ -183,7 +208,7 @@ def test_nesting_cassette_context_managers(*args):
|
||||
)
|
||||
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")
|
||||
@@ -285,16 +310,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 +333,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]
|
||||
|
||||
@@ -220,6 +220,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},
|
||||
|
||||
@@ -44,4 +44,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,11 +1,12 @@
|
||||
from unittest import mock
|
||||
|
||||
from vcr import mode
|
||||
from vcr.stubs import VCRHTTPSConnection
|
||||
from vcr.cassette import Cassette
|
||||
|
||||
|
||||
class TestVCRConnection:
|
||||
def test_setting_of_attributes_get_propogated_to_real_connection(self):
|
||||
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"
|
||||
@@ -13,6 +14,6 @@ class TestVCRConnection:
|
||||
@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
|
||||
|
||||
@@ -4,7 +4,7 @@ import os
|
||||
import pytest
|
||||
import http.client as httplib
|
||||
|
||||
from vcr import VCR, use_cassette
|
||||
from vcr import VCR, mode, use_cassette
|
||||
from vcr.request import Request
|
||||
from vcr.stubs import VCRHTTPSConnection
|
||||
from vcr.patch import _HTTPConnection, force_reset
|
||||
@@ -31,7 +31,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()
|
||||
|
||||
@@ -188,11 +188,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 +201,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 +212,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 +226,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 +237,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 +248,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,7 +259,7 @@ 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"
|
||||
)
|
||||
|
||||
40
tox.ini
40
tox.ini
@@ -1,13 +1,22 @@
|
||||
[tox]
|
||||
skip_missing_interpreters=true
|
||||
envlist =
|
||||
envlist =
|
||||
cov-clean,
|
||||
lint,
|
||||
{py35,py36,py37,py38}-{requests,httplib2,urllib3,tornado4,boto3,aiohttp},
|
||||
{py37,py38,py39,py310}-{requests,httplib2,urllib3,tornado4,boto3,aiohttp,httpx},
|
||||
{pypy3}-{requests,httplib2,urllib3,tornado4,boto3},
|
||||
{py310}-httpx019,
|
||||
cov-report
|
||||
|
||||
|
||||
[gh-actions]
|
||||
python =
|
||||
3.7: py37, lint
|
||||
3.8: py38
|
||||
3.9: py39
|
||||
3.10: py310
|
||||
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 +27,7 @@ commands = coverage erase
|
||||
[testenv:cov-report]
|
||||
deps = coverage
|
||||
skip_install=true
|
||||
commands =
|
||||
commands =
|
||||
coverage html
|
||||
coverage report --fail-under=90
|
||||
|
||||
@@ -30,9 +39,10 @@ commands =
|
||||
flake8 --version
|
||||
flake8 --exclude=./docs/conf.py,./.tox/
|
||||
pyflakes ./docs/conf.py
|
||||
deps =
|
||||
flake8
|
||||
deps =
|
||||
flake8
|
||||
black
|
||||
basepython = python3.7
|
||||
|
||||
[testenv:docs]
|
||||
# Running sphinx from inside the "docs" directory
|
||||
@@ -62,26 +72,30 @@ usedevelop=true
|
||||
commands =
|
||||
./runtests.sh --cov=./vcr --cov-branch --cov-report=xml --cov-append {posargs}
|
||||
deps =
|
||||
Flask
|
||||
Werkzeug==2.0.3
|
||||
pytest
|
||||
pytest-httpbin
|
||||
git+https://github.com/immerrr/pytest-httpbin@fix-redirect-location-scheme-for-secure-server
|
||||
pytest-cov
|
||||
PyYAML
|
||||
ipaddress
|
||||
requests: requests>=2.22.0
|
||||
httplib2: httplib2
|
||||
urllib3: urllib3
|
||||
{py35,py36}-tornado4: tornado>=4,<5
|
||||
{py35,py36}-tornado4: pytest-tornado
|
||||
{py35,py36}-tornado4: pycurl
|
||||
boto3: boto3
|
||||
boto3: urllib3
|
||||
aiohttp: aiohttp
|
||||
aiohttp: pytest-asyncio
|
||||
aiohttp: pytest-aiohttp
|
||||
depends =
|
||||
lint,{py35,py36,py37,py38,pypy3}-{requests,httplib2,urllib3,tornado4,boto3},{py35,py36,py37,py38}-{aiohttp}: cov-clean
|
||||
cov-report: lint,{py35,py36,py37,py38,pypy3}-{requests,httplib2,urllib3,tornado4,boto3},{py35,py36,py37,py38}-{aiohttp}
|
||||
httpx: httpx
|
||||
{py37,py38,py39,py310}-{httpx}: httpx
|
||||
{py37,py38,py39,py310}-{httpx}: pytest-asyncio
|
||||
httpx: httpx>0.19
|
||||
# httpx==0.19 is the latest version that supports allow_redirects, newer versions use follow_redirects
|
||||
httpx019: httpx==0.19
|
||||
{py37,py38,py39,py310}-{httpx}: pytest-asyncio
|
||||
depends =
|
||||
lint,{py37,py38,py39,py310,pypy3}-{requests,httplib2,urllib3,tornado4,boto3},{py37,py38,py39,py310}-{aiohttp},{py37,py38,py39,py310}-{httpx}: cov-clean
|
||||
cov-report: lint,{py37,py38,py39,py310,pypy3}-{requests,httplib2,urllib3,tornado4,boto3},{py37,py38,py39,py310}-{aiohttp}
|
||||
passenv =
|
||||
AWS_ACCESS_KEY_ID
|
||||
AWS_DEFAULT_REGION
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import logging
|
||||
from .config import VCR
|
||||
from logging import NullHandler
|
||||
from .record_mode import RecordMode as mode # noqa import is not used in this file
|
||||
|
||||
__version__ = "4.0.2"
|
||||
__version__ = "4.2.0"
|
||||
|
||||
logging.getLogger(__name__).addHandler(NullHandler())
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ from .serializers import yamlserializer
|
||||
from .persisters.filesystem import FilesystemPersister
|
||||
from .util import partition_dict
|
||||
from ._handle_coroutine import handle_coroutine
|
||||
from .record_mode import RecordMode
|
||||
|
||||
try:
|
||||
from asyncio import iscoroutinefunction
|
||||
@@ -175,12 +176,13 @@ class Cassette:
|
||||
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
|
||||
@@ -192,6 +194,7 @@ class Cassette:
|
||||
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 = []
|
||||
@@ -206,7 +209,7 @@ class Cassette:
|
||||
@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):
|
||||
@@ -218,7 +221,7 @@ class Cassette:
|
||||
|
||||
@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"""
|
||||
@@ -250,7 +253,7 @@ class Cassette:
|
||||
|
||||
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):
|
||||
"""
|
||||
@@ -258,7 +261,7 @@ class Cassette:
|
||||
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.
|
||||
@@ -348,6 +351,6 @@ class Cassette:
|
||||
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:
|
||||
if self.play_counts[index] == 0 or self.allow_playback_repeats:
|
||||
return True
|
||||
return False
|
||||
|
||||
@@ -12,6 +12,7 @@ from .cassette import Cassette
|
||||
from .serializers import yamlserializer, jsonserializer
|
||||
from .persisters.filesystem import FilesystemPersister
|
||||
from .util import compose, auto_decorate
|
||||
from .record_mode import RecordMode
|
||||
from . import matchers
|
||||
from . import filters
|
||||
|
||||
@@ -37,7 +38,7 @@ class VCR:
|
||||
custom_patches=(),
|
||||
filter_query_parameters=(),
|
||||
ignore_hosts=(),
|
||||
record_mode="once",
|
||||
record_mode=RecordMode.ONCE,
|
||||
ignore_localhost=False,
|
||||
filter_headers=(),
|
||||
before_record_response=None,
|
||||
@@ -148,6 +149,7 @@ class VCR:
|
||||
"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),
|
||||
}
|
||||
path = kwargs.get("path")
|
||||
if path:
|
||||
|
||||
@@ -84,7 +84,17 @@ 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":
|
||||
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.decode("utf-8"))
|
||||
for k, rv in replacements.items():
|
||||
if k in json_data:
|
||||
|
||||
25
vcr/patch.py
25
vcr/patch.py
@@ -94,6 +94,15 @@ else:
|
||||
_AiohttpClientSessionRequest = aiohttp.client.ClientSession._request
|
||||
|
||||
|
||||
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)
|
||||
@@ -116,6 +125,7 @@ class CassettePatcherBuilder:
|
||||
self._boto(),
|
||||
self._tornado(),
|
||||
self._aiohttp(),
|
||||
self._httpx(),
|
||||
self._build_patchers_from_mock_triples(self._cassette.custom_patches),
|
||||
)
|
||||
|
||||
@@ -313,6 +323,21 @@ class CassettePatcherBuilder:
|
||||
new_request = vcr_request(self._cassette, _AiohttpClientSessionRequest)
|
||||
yield client.ClientSession, "_request", new_request
|
||||
|
||||
@_build_patchers_from_mock_triples_decorator
|
||||
def _httpx(self):
|
||||
try:
|
||||
import httpx
|
||||
except ImportError: # pragma: no cover
|
||||
return
|
||||
else:
|
||||
from .stubs.httpx_stubs import async_vcr_send, sync_vcr_send
|
||||
|
||||
new_async_client_send = async_vcr_send(self._cassette, _HttpxAsyncClient_send)
|
||||
yield httpx.AsyncClient, "send", new_async_client_send
|
||||
|
||||
new_sync_client_send = sync_vcr_send(self._cassette, _HttpxSyncClient_send)
|
||||
yield httpx.Client, "send", new_sync_client_send
|
||||
|
||||
def _urllib3_patchers(self, cpool, stubs):
|
||||
http_connection_remover = ConnectionRemover(
|
||||
self._get_cassette_subclass(stubs.VCRRequestsHTTPConnection)
|
||||
|
||||
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"
|
||||
@@ -16,7 +16,7 @@ def serialize(cassette_dict):
|
||||
)
|
||||
|
||||
try:
|
||||
return json.dumps(cassette_dict, indent=4)
|
||||
return json.dumps(cassette_dict, indent=4) + "\n"
|
||||
except UnicodeDecodeError as original: # py2
|
||||
raise UnicodeDecodeError(
|
||||
original.encoding,
|
||||
|
||||
@@ -314,7 +314,7 @@ class VCRConnection:
|
||||
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
|
||||
|
||||
@@ -5,9 +5,14 @@ import logging
|
||||
import json
|
||||
|
||||
from aiohttp import ClientConnectionError, ClientResponse, RequestInfo, streams
|
||||
from multidict import CIMultiDict, CIMultiDictProxy
|
||||
from aiohttp import hdrs, CookieJar
|
||||
from http.cookies import CookieError, Morsel, SimpleCookie
|
||||
from aiohttp.helpers import strip_auth_from_url
|
||||
from multidict import CIMultiDict, CIMultiDictProxy, MultiDict
|
||||
from typing import Union, Mapping
|
||||
from yarl import URL
|
||||
|
||||
from vcr.errors import CannotOverwriteExistingCassetteException
|
||||
from vcr.request import Request
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
@@ -59,15 +64,27 @@ def build_response(vcr_request, vcr_response, history):
|
||||
request_info = RequestInfo(
|
||||
url=URL(vcr_request.url),
|
||||
method=vcr_request.method,
|
||||
headers=CIMultiDictProxy(CIMultiDict(vcr_request.headers)),
|
||||
headers=_deserialize_headers(vcr_request.headers),
|
||||
real_url=URL(vcr_request.url),
|
||||
)
|
||||
response = MockClientResponse(vcr_request.method, URL(vcr_response.get("url")), request_info=request_info)
|
||||
response.status = vcr_response["status"]["code"]
|
||||
response._body = vcr_response["body"].get("string", b"")
|
||||
response.reason = vcr_response["status"]["message"]
|
||||
response._headers = CIMultiDictProxy(CIMultiDict(vcr_response["headers"]))
|
||||
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
|
||||
@@ -75,31 +92,54 @@ def build_response(vcr_request, vcr_response, history):
|
||||
|
||||
def _serialize_headers(headers):
|
||||
"""Serialize CIMultiDictProxy to a pickle-able dict because proxy
|
||||
objects forbid pickling:
|
||||
objects forbid pickling:
|
||||
|
||||
https://github.com/aio-libs/multidict/issues/340
|
||||
https://github.com/aio-libs/multidict/issues/340
|
||||
"""
|
||||
# Mark strings as keys so 'istr' types don't show up in
|
||||
# the cassettes as comments.
|
||||
return {str(k): v for k, v in headers.items()}
|
||||
serialized_headers = {}
|
||||
for k, v in headers.items():
|
||||
serialized_headers.setdefault(str(k), []).append(v)
|
||||
|
||||
return serialized_headers
|
||||
|
||||
|
||||
def play_responses(cassette, vcr_request):
|
||||
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 300 <= response.status <= 399:
|
||||
next_url = URL(response.url).with_path(response.headers["location"])
|
||||
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_request = cassette.find_requests_with_most_matches(vcr_request)[0][0]
|
||||
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.
|
||||
@@ -132,9 +172,9 @@ async def record_response(cassette, vcr_request, 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.
|
||||
them by default. This method is used to write individual
|
||||
request-response chains that were implicitly followed to get
|
||||
to the final destination.
|
||||
"""
|
||||
|
||||
for past_response in response.history:
|
||||
@@ -163,6 +203,43 @@ async def record_responses(cassette, vcr_request, response):
|
||||
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):
|
||||
@@ -171,32 +248,29 @@ def vcr_request(cassette, real_request):
|
||||
headers = self._prepare_headers(headers)
|
||||
data = kwargs.get("data", kwargs.get("json"))
|
||||
params = kwargs.get("params")
|
||||
cookies = kwargs.get("cookies")
|
||||
|
||||
if auth is not None:
|
||||
headers["AUTHORIZATION"] = auth.encode()
|
||||
|
||||
request_url = URL(url)
|
||||
if params:
|
||||
for k, v in params.items():
|
||||
params[k] = str(v)
|
||||
request_url = URL(url).with_query(params)
|
||||
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, headers)
|
||||
vcr_request = Request(method, str(request_url), data, _serialize_headers(headers))
|
||||
|
||||
if cassette.can_play_response_for(vcr_request):
|
||||
return play_responses(cassette, vcr_request)
|
||||
log.info("Playing response for {} from cassette".format(vcr_request))
|
||||
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):
|
||||
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
|
||||
raise CannotOverwriteExistingCassetteException(cassette=cassette, failed_request=vcr_request)
|
||||
|
||||
log.info("%s not in cassette, sending to real server", vcr_request)
|
||||
|
||||
170
vcr/stubs/httpx_stubs.py
Normal file
170
vcr/stubs/httpx_stubs.py
Normal file
@@ -0,0 +1,170 @@
|
||||
import functools
|
||||
import logging
|
||||
from unittest.mock import patch, MagicMock
|
||||
|
||||
import httpx
|
||||
from vcr.request import Request as VcrRequest
|
||||
from vcr.errors import CannotOverwriteExistingCassetteException
|
||||
import inspect
|
||||
|
||||
_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
|
||||
Reference in New Issue
Block a user