mirror of
https://github.com/kevin1024/vcrpy.git
synced 2025-12-09 17:15:35 +00:00
Compare commits
89 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f3f66086a1 | ||
|
|
c3767c2fdb | ||
|
|
6fff3ab952 | ||
|
|
35378d046f | ||
|
|
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
|
all HTTP interactions, which will update them to correspond to the new
|
||||||
API.
|
API.
|
||||||
|
|
||||||
|
Usage with Pytest
|
||||||
|
-----------------
|
||||||
|
|
||||||
|
There is a library to provide some pytest fixtures called pytest-recording https://github.com/kiwicom/pytest-recording
|
||||||
|
|
||||||
License
|
License
|
||||||
-------
|
-------
|
||||||
|
|
||||||
@@ -57,8 +62,8 @@ more details
|
|||||||
:target: https://pypi.python.org/pypi/vcrpy
|
:target: https://pypi.python.org/pypi/vcrpy
|
||||||
.. |Python versions| image:: https://img.shields.io/pypi/pyversions/vcrpy.svg
|
.. |Python versions| image:: https://img.shields.io/pypi/pyversions/vcrpy.svg
|
||||||
:target: https://pypi.python.org/pypi/vcrpy
|
:target: https://pypi.python.org/pypi/vcrpy
|
||||||
.. |Build Status| image:: https://secure.travis-ci.org/kevin1024/vcrpy.svg?branch=master
|
.. |Build Status| image:: https://github.com/kevin1024/vcrpy/actions/workflows/main.yml/badge.svg
|
||||||
:target: http://travis-ci.org/kevin1024/vcrpy
|
:target: https://github.com/kevin1024/vcrpy/actions
|
||||||
.. |Gitter| image:: https://badges.gitter.im/Join%20Chat.svg
|
.. |Gitter| image:: https://badges.gitter.im/Join%20Chat.svg
|
||||||
:alt: Join the chat at https://gitter.im/kevin1024/vcrpy
|
: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
|
: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.
|
been played back.
|
||||||
- ``responses_of(request)``: Access the responses that match a given
|
- ``responses_of(request)``: Access the responses that match a given
|
||||||
request
|
request
|
||||||
|
- ``allow_playback_repeats``: A boolean indicating whether responses
|
||||||
|
can be played back more than once.
|
||||||
|
|
||||||
The ``Request`` object has the following properties:
|
The ``Request`` object has the following properties:
|
||||||
|
|
||||||
@@ -269,7 +271,7 @@ You can also do response filtering with the
|
|||||||
similar to the above ``before_record_request`` - you can
|
similar to the above ``before_record_request`` - you can
|
||||||
mutate the response, or return ``None`` to avoid recording
|
mutate the response, or return ``None`` to avoid recording
|
||||||
the request and response altogether. For example to hide
|
the request and response altogether. For example to hide
|
||||||
sensitive data from the request body:
|
sensitive data from the response body:
|
||||||
|
|
||||||
.. code:: python
|
.. 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
|
assert cass.all_played
|
||||||
cass.rewind()
|
cass.rewind()
|
||||||
assert not cass.all_played
|
assert not cass.all_played
|
||||||
|
|
||||||
|
Playback Repeats
|
||||||
|
----------------
|
||||||
|
|
||||||
|
By default, each response in a cassette can only be matched and played back
|
||||||
|
once while the cassette is in use, unless the cassette is rewound.
|
||||||
|
|
||||||
|
If you want to allow playback repeats without rewinding the cassette, use
|
||||||
|
the Cassette ``allow_playback_repeats`` option.
|
||||||
|
|
||||||
|
.. code:: python
|
||||||
|
|
||||||
|
with vcr.use_cassette('fixtures/vcr_cassettes/synopsis.yaml', allow_playback_repeats=True) as cass:
|
||||||
|
for x in range(10):
|
||||||
|
response = urllib2.urlopen('http://www.zombo.com/').read()
|
||||||
|
assert cass.all_played
|
||||||
|
|||||||
@@ -1,14 +1,34 @@
|
|||||||
Changelog
|
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>`_
|
`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.
|
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.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
|
- 4.0.2
|
||||||
- Fix mock imports as reported in #504 by @llybin. Thank you.
|
- Fix mock imports as reported in #504 by @llybin. Thank you.
|
||||||
- 4.0.1
|
- 4.0.1
|
||||||
@@ -92,7 +112,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).
|
- decode_compressed_response option and filter (thanks @jayvdb).
|
||||||
- 1.7.4 [#217]
|
- 1.7.4 [#217]
|
||||||
- Make use_cassette decorated functions actually return a value (thanks @bcen).
|
- 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.
|
- Better headers dictionary management.
|
||||||
- 1.7.3 [#188]
|
- 1.7.3 [#188]
|
||||||
- ``additional_matchers`` kwarg on ``use_cassette``.
|
- ``additional_matchers`` kwarg on ``use_cassette``.
|
||||||
@@ -195,7 +215,7 @@ All help in providing PRs to close out bug issues is appreciated. Even if that i
|
|||||||
- 0.3.4
|
- 0.3.4
|
||||||
- Bugfix: close file before renaming it. This fixes an issue on Windows. Thanks @smallcode for the fix.
|
- Bugfix: close file before renaming it. This fixes an issue on Windows. Thanks @smallcode for the fix.
|
||||||
- 0.3.3
|
- 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
|
- 0.3.2
|
||||||
- Fix issue with new config syntax and the ``match_on`` parameter. Thanks, @chromy!
|
- Fix issue with new config syntax and the ``match_on`` parameter. Thanks, @chromy!
|
||||||
- 0.3.1
|
- 0.3.1
|
||||||
|
|||||||
@@ -42,7 +42,8 @@ This can be configured by changing the ``match_on`` setting.
|
|||||||
The following options are available :
|
The following options are available :
|
||||||
|
|
||||||
- method (for example, POST or GET)
|
- 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)
|
- host (the hostname of the server receiving the request)
|
||||||
- port (the port of the server receiving the request)
|
- port (the port of the server receiving the request)
|
||||||
- path (the path of the request)
|
- path (the path of the request)
|
||||||
|
|||||||
@@ -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 {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
|
tox -e py37-requests -- -v --last-failed
|
||||||
|
|
||||||
This will run only tests that look like ``test_status_code`` or
|
This will run only tests that look like ``test_status_code`` or
|
||||||
``test_gzip`` in the test suite, and only in the python 3.6 environment
|
``test_gzip`` in the test suite, and only in the python 3.7 environment
|
||||||
that has ``requests`` installed.
|
that has ``requests`` installed.
|
||||||
|
|
||||||
Also, in order for the boto tests to run, you will need an AWS key.
|
Also, in order for the boto tests to run, you will need an AWS key.
|
||||||
@@ -130,10 +130,10 @@ in this example::
|
|||||||
pip install tox tox-pyenv
|
pip install tox tox-pyenv
|
||||||
|
|
||||||
# Install supported versions (at time of writing), this does not activate them
|
# Install supported versions (at time of writing), this does not activate them
|
||||||
pyenv install 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
|
# 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
|
# Run the whole test suite
|
||||||
tox
|
tox
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ with pip::
|
|||||||
Compatibility
|
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:
|
The following HTTP libraries are supported:
|
||||||
|
|
||||||
@@ -22,6 +22,7 @@ The following HTTP libraries are supported:
|
|||||||
- ``tornado.httpclient``
|
- ``tornado.httpclient``
|
||||||
- ``urllib2``
|
- ``urllib2``
|
||||||
- ``urllib3``
|
- ``urllib3``
|
||||||
|
- ``httpx``
|
||||||
|
|
||||||
Speed
|
Speed
|
||||||
-----
|
-----
|
||||||
|
|||||||
@@ -4,10 +4,10 @@ Usage
|
|||||||
.. code:: python
|
.. code:: python
|
||||||
|
|
||||||
import vcr
|
import vcr
|
||||||
import urllib2
|
import urllib
|
||||||
|
|
||||||
with vcr.use_cassette('fixtures/vcr_cassettes/synopsis.yaml'):
|
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
|
assert 'Example domains' in response
|
||||||
|
|
||||||
Run this test once, and VCR.py will record the HTTP request to
|
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')
|
@vcr.use_cassette('fixtures/vcr_cassettes/synopsis.yaml')
|
||||||
def test_iana():
|
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
|
assert 'Example domains' in response
|
||||||
|
|
||||||
When using the decorator version of ``use_cassette``, it is possible to
|
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()
|
@vcr.use_cassette()
|
||||||
def test_iana():
|
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
|
assert 'Example domains' in response
|
||||||
|
|
||||||
In this case, the cassette file will be given the same name as the test
|
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
|
# If you are getting an INVOCATION ERROR for this script then there is
|
||||||
# a good chance you are running on Windows.
|
# a good chance you are running on Windows.
|
||||||
# You can and should use WSL for running tox on Windows when it calls bash scripts.
|
# You can and should use WSL for running tox on Windows when it calls bash scripts.
|
||||||
REQUESTS_CA_BUNDLE=`python -m pytest_httpbin.certs` py.test $*
|
REQUESTS_CA_BUNDLE=`python -m pytest_httpbin.certs` pytest $*
|
||||||
|
|||||||
9
setup.py
9
setup.py
@@ -46,8 +46,7 @@ install_requires = [
|
|||||||
"PyYAML",
|
"PyYAML",
|
||||||
"wrapt",
|
"wrapt",
|
||||||
"six>=1.5",
|
"six>=1.5",
|
||||||
'yarl; python_version>="3.6"',
|
"yarl",
|
||||||
'yarl<1.4; python_version=="3.5"',
|
|
||||||
]
|
]
|
||||||
|
|
||||||
setup(
|
setup(
|
||||||
@@ -60,7 +59,7 @@ setup(
|
|||||||
author_email="me@kevinmccarthy.org",
|
author_email="me@kevinmccarthy.org",
|
||||||
url="https://github.com/kevin1024/vcrpy",
|
url="https://github.com/kevin1024/vcrpy",
|
||||||
packages=find_packages(exclude=["tests*"]),
|
packages=find_packages(exclude=["tests*"]),
|
||||||
python_requires=">=3.5",
|
python_requires=">=3.7",
|
||||||
install_requires=install_requires,
|
install_requires=install_requires,
|
||||||
license="MIT",
|
license="MIT",
|
||||||
tests_require=["pytest", "mock", "pytest-httpbin"],
|
tests_require=["pytest", "mock", "pytest-httpbin"],
|
||||||
@@ -70,10 +69,10 @@ setup(
|
|||||||
"Intended Audience :: Developers",
|
"Intended Audience :: Developers",
|
||||||
"Programming Language :: Python",
|
"Programming Language :: Python",
|
||||||
"Programming Language :: Python :: 3",
|
"Programming Language :: Python :: 3",
|
||||||
"Programming Language :: Python :: 3.5",
|
|
||||||
"Programming Language :: Python :: 3.6",
|
|
||||||
"Programming Language :: Python :: 3.7",
|
"Programming Language :: Python :: 3.7",
|
||||||
"Programming Language :: Python :: 3.8",
|
"Programming Language :: Python :: 3.8",
|
||||||
|
"Programming Language :: Python :: 3.9",
|
||||||
|
"Programming Language :: Python :: 3.10",
|
||||||
"Programming Language :: Python :: 3 :: Only",
|
"Programming Language :: Python :: 3 :: Only",
|
||||||
"Programming Language :: Python :: Implementation :: CPython",
|
"Programming Language :: Python :: Implementation :: CPython",
|
||||||
"Programming Language :: Python :: Implementation :: PyPy",
|
"Programming Language :: Python :: Implementation :: PyPy",
|
||||||
|
|||||||
@@ -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 contextlib
|
||||||
import logging
|
import logging
|
||||||
|
import urllib.parse
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
@@ -151,12 +152,13 @@ def test_post(tmpdir, scheme, body, caplog):
|
|||||||
|
|
||||||
|
|
||||||
def test_params(tmpdir, scheme):
|
def test_params(tmpdir, scheme):
|
||||||
url = scheme + "://httpbin.org/get"
|
url = scheme + "://httpbin.org/get?d=d"
|
||||||
headers = {"Content-Type": "application/json"}
|
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:
|
with vcr.use_cassette(str(tmpdir.join("get.yaml"))) as cassette:
|
||||||
_, response_json = get(url, output="json", params=params, headers=headers)
|
_, 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:
|
with vcr.use_cassette(str(tmpdir.join("get.yaml"))) as cassette:
|
||||||
_, cassette_response_json = get(url, output="json", params=params, headers=headers)
|
_, 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):
|
def test_params_same_url_distinct_params(tmpdir, scheme):
|
||||||
url = scheme + "://httpbin.org/get"
|
url = scheme + "://httpbin.org/get"
|
||||||
headers = {"Content-Type": "application/json"}
|
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:
|
with vcr.use_cassette(str(tmpdir.join("get.yaml"))) as cassette:
|
||||||
_, response_json = get(url, output="json", params=params, headers=headers)
|
_, 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"}
|
other_params = {"other": "params"}
|
||||||
with vcr.use_cassette(str(tmpdir.join("get.yaml"))) as cassette:
|
with vcr.use_cassette(str(tmpdir.join("get.yaml"))) as cassette:
|
||||||
response, cassette_response_text = get(url, output="text", params=other_params)
|
with pytest.raises(vcr.errors.CannotOverwriteExistingCassetteException):
|
||||||
assert "No match for the request" in cassette_response_text
|
get(url, output="text", params=other_params)
|
||||||
assert response.status == 599
|
|
||||||
|
|
||||||
|
|
||||||
def test_params_on_url(tmpdir, scheme):
|
def test_params_on_url(tmpdir, scheme):
|
||||||
@@ -250,7 +251,7 @@ def test_aiohttp_test_client_json(aiohttp_client, tmpdir):
|
|||||||
|
|
||||||
|
|
||||||
def test_redirect(aiohttp_client, tmpdir):
|
def test_redirect(aiohttp_client, tmpdir):
|
||||||
url = "https://httpbin.org/redirect/2"
|
url = "https://mockbin.org/redirect/302/2"
|
||||||
|
|
||||||
with vcr.use_cassette(str(tmpdir.join("redirect.yaml"))):
|
with vcr.use_cassette(str(tmpdir.join("redirect.yaml"))):
|
||||||
response, _ = get(url)
|
response, _ = get(url)
|
||||||
@@ -273,11 +274,28 @@ def test_redirect(aiohttp_client, tmpdir):
|
|||||||
assert cassette_response.request_info.real_url == response.request_info.real_url
|
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):
|
def test_double_requests(tmpdir):
|
||||||
"""We should capture, record, and replay all requests and response chains,
|
"""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"
|
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.
|
# Now that we made both requests, we should have played both.
|
||||||
assert cassette.play_count == 2
|
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):
|
def test_nonexistent_directory(tmpdir, httpbin):
|
||||||
"""If we load a cassette in a nonexistent directory, it can save ok"""
|
"""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")))
|
assert not os.path.exists(str(tmpdir.join("nonexistent")))
|
||||||
|
|
||||||
# Run VCR to create dir and cassette file
|
# Run VCR to create dir and cassette file
|
||||||
|
|||||||
@@ -13,7 +13,9 @@ def test_set_serializer_default_config(tmpdir, httpbin):
|
|||||||
urlopen(httpbin.url + "/get")
|
urlopen(httpbin.url + "/get")
|
||||||
|
|
||||||
with open(str(tmpdir.join("test.json"))) as f:
|
with open(str(tmpdir.join("test.json"))) as f:
|
||||||
assert json.loads(f.read())
|
file_content = f.read()
|
||||||
|
assert file_content.endswith("\n")
|
||||||
|
assert json.loads(file_content)
|
||||||
|
|
||||||
|
|
||||||
def test_default_set_cassette_library_dir(tmpdir, httpbin):
|
def test_default_set_cassette_library_dir(tmpdir, httpbin):
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ def test_disk_saver_write(tmpdir, httpbin):
|
|||||||
# the mtime doesn't change
|
# the mtime doesn't change
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
|
|
||||||
with vcr.use_cassette(fname, record_mode="any") as cass:
|
with vcr.use_cassette(fname, record_mode=vcr.mode.ANY) as cass:
|
||||||
urlopen(httpbin.url).read()
|
urlopen(httpbin.url).read()
|
||||||
urlopen(httpbin.url + "/get").read()
|
urlopen(httpbin.url + "/get").read()
|
||||||
assert cass.play_count == 1
|
assert cass.play_count == 1
|
||||||
|
|||||||
@@ -61,13 +61,14 @@ def test_response_headers(tmpdir, httpbin_both):
|
|||||||
assert set(headers) == set(resp.items())
|
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"""
|
"""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"))):
|
with vcr.use_cassette(str(tmpdir.join("headers.yaml"))):
|
||||||
resp, _ = http().request(url)
|
resp, _ = http().request(url)
|
||||||
effective_url = resp["content-location"]
|
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"))):
|
with vcr.use_cassette(str(tmpdir.join("headers.yaml"))):
|
||||||
resp, _ = http().request(url)
|
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
|
@pytest.fixture
|
||||||
def cassette(tmpdir, httpbin, httpbin_secure):
|
def cassette(tmpdir, httpbin, httpbin_secure):
|
||||||
"""
|
"""
|
||||||
Helper fixture used to prepare the cassete
|
Helper fixture used to prepare the cassette
|
||||||
returns path to the recorded cassette
|
returns path to the recorded cassette
|
||||||
"""
|
"""
|
||||||
default_uri = _replace_httpbin(DEFAULT_URI, httpbin, httpbin_secure)
|
default_uri = _replace_httpbin(DEFAULT_URI, httpbin, httpbin_secure)
|
||||||
|
|
||||||
cassette_path = str(tmpdir.join("test.yml"))
|
cassette_path = str(tmpdir.join("test.yml"))
|
||||||
with vcr.use_cassette(cassette_path, record_mode="all"):
|
with vcr.use_cassette(cassette_path, record_mode=vcr.mode.ALL):
|
||||||
urlopen(default_uri)
|
urlopen(default_uri)
|
||||||
return cassette_path
|
return cassette_path
|
||||||
|
|
||||||
|
|||||||
@@ -5,11 +5,11 @@ from urllib.request import urlopen
|
|||||||
|
|
||||||
def test_once_record_mode(tmpdir, httpbin):
|
def test_once_record_mode(tmpdir, httpbin):
|
||||||
testfile = str(tmpdir.join("recordmode.yml"))
|
testfile = str(tmpdir.join("recordmode.yml"))
|
||||||
with vcr.use_cassette(testfile, record_mode="once"):
|
with vcr.use_cassette(testfile, record_mode=vcr.mode.ONCE):
|
||||||
# cassette file doesn't exist, so create.
|
# cassette file doesn't exist, so create.
|
||||||
urlopen(httpbin.url).read()
|
urlopen(httpbin.url).read()
|
||||||
|
|
||||||
with vcr.use_cassette(testfile, record_mode="once"):
|
with vcr.use_cassette(testfile, record_mode=vcr.mode.ONCE):
|
||||||
# make the same request again
|
# make the same request again
|
||||||
urlopen(httpbin.url).read()
|
urlopen(httpbin.url).read()
|
||||||
|
|
||||||
@@ -22,12 +22,12 @@ def test_once_record_mode(tmpdir, httpbin):
|
|||||||
|
|
||||||
def test_once_record_mode_two_times(tmpdir, httpbin):
|
def test_once_record_mode_two_times(tmpdir, httpbin):
|
||||||
testfile = str(tmpdir.join("recordmode.yml"))
|
testfile = str(tmpdir.join("recordmode.yml"))
|
||||||
with vcr.use_cassette(testfile, record_mode="once"):
|
with vcr.use_cassette(testfile, record_mode=vcr.mode.ONCE):
|
||||||
# get two of the same file
|
# get two of the same file
|
||||||
urlopen(httpbin.url).read()
|
urlopen(httpbin.url).read()
|
||||||
urlopen(httpbin.url).read()
|
urlopen(httpbin.url).read()
|
||||||
|
|
||||||
with vcr.use_cassette(testfile, record_mode="once"):
|
with vcr.use_cassette(testfile, record_mode=vcr.mode.ONCE):
|
||||||
# do it again
|
# do it again
|
||||||
urlopen(httpbin.url).read()
|
urlopen(httpbin.url).read()
|
||||||
urlopen(httpbin.url).read()
|
urlopen(httpbin.url).read()
|
||||||
@@ -35,7 +35,7 @@ def test_once_record_mode_two_times(tmpdir, httpbin):
|
|||||||
|
|
||||||
def test_once_mode_three_times(tmpdir, httpbin):
|
def test_once_mode_three_times(tmpdir, httpbin):
|
||||||
testfile = str(tmpdir.join("recordmode.yml"))
|
testfile = str(tmpdir.join("recordmode.yml"))
|
||||||
with vcr.use_cassette(testfile, record_mode="once"):
|
with vcr.use_cassette(testfile, record_mode=vcr.mode.ONCE):
|
||||||
# get three of the same file
|
# get three of the same file
|
||||||
urlopen(httpbin.url).read()
|
urlopen(httpbin.url).read()
|
||||||
urlopen(httpbin.url).read()
|
urlopen(httpbin.url).read()
|
||||||
@@ -45,11 +45,11 @@ def test_once_mode_three_times(tmpdir, httpbin):
|
|||||||
def test_new_episodes_record_mode(tmpdir, httpbin):
|
def test_new_episodes_record_mode(tmpdir, httpbin):
|
||||||
testfile = str(tmpdir.join("recordmode.yml"))
|
testfile = str(tmpdir.join("recordmode.yml"))
|
||||||
|
|
||||||
with vcr.use_cassette(testfile, record_mode="new_episodes"):
|
with vcr.use_cassette(testfile, record_mode=vcr.mode.NEW_EPISODES):
|
||||||
# cassette file doesn't exist, so create.
|
# cassette file doesn't exist, so create.
|
||||||
urlopen(httpbin.url).read()
|
urlopen(httpbin.url).read()
|
||||||
|
|
||||||
with vcr.use_cassette(testfile, record_mode="new_episodes") as cass:
|
with vcr.use_cassette(testfile, record_mode=vcr.mode.NEW_EPISODES) as cass:
|
||||||
# make the same request again
|
# make the same request again
|
||||||
urlopen(httpbin.url).read()
|
urlopen(httpbin.url).read()
|
||||||
|
|
||||||
@@ -57,7 +57,7 @@ def test_new_episodes_record_mode(tmpdir, httpbin):
|
|||||||
assert cass.all_played
|
assert cass.all_played
|
||||||
|
|
||||||
# in the "new_episodes" record mode, we can add more requests to
|
# 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()
|
urlopen(httpbin.url + "/get").read()
|
||||||
|
|
||||||
# one of the responses has been played
|
# one of the responses has been played
|
||||||
@@ -66,7 +66,7 @@ def test_new_episodes_record_mode(tmpdir, httpbin):
|
|||||||
# not all responses have been played
|
# not all responses have been played
|
||||||
assert not cass.all_played
|
assert not cass.all_played
|
||||||
|
|
||||||
with vcr.use_cassette(testfile, record_mode="new_episodes") as cass:
|
with vcr.use_cassette(testfile, record_mode=vcr.mode.NEW_EPISODES) as cass:
|
||||||
# the cassette should now have 2 responses
|
# the cassette should now have 2 responses
|
||||||
assert len(cass.responses) == 2
|
assert len(cass.responses) == 2
|
||||||
|
|
||||||
@@ -74,11 +74,11 @@ def test_new_episodes_record_mode(tmpdir, httpbin):
|
|||||||
def test_new_episodes_record_mode_two_times(tmpdir, httpbin):
|
def test_new_episodes_record_mode_two_times(tmpdir, httpbin):
|
||||||
testfile = str(tmpdir.join("recordmode.yml"))
|
testfile = str(tmpdir.join("recordmode.yml"))
|
||||||
url = httpbin.url + "/bytes/1024"
|
url = httpbin.url + "/bytes/1024"
|
||||||
with vcr.use_cassette(testfile, record_mode="new_episodes"):
|
with vcr.use_cassette(testfile, record_mode=vcr.mode.NEW_EPISODES):
|
||||||
# cassette file doesn't exist, so create.
|
# cassette file doesn't exist, so create.
|
||||||
original_first_response = urlopen(url).read()
|
original_first_response = urlopen(url).read()
|
||||||
|
|
||||||
with vcr.use_cassette(testfile, record_mode="new_episodes"):
|
with vcr.use_cassette(testfile, record_mode=vcr.mode.NEW_EPISODES):
|
||||||
# make the same request again
|
# make the same request again
|
||||||
assert urlopen(url).read() == original_first_response
|
assert urlopen(url).read() == original_first_response
|
||||||
|
|
||||||
@@ -86,7 +86,7 @@ def test_new_episodes_record_mode_two_times(tmpdir, httpbin):
|
|||||||
# to the cassette without repercussions
|
# to the cassette without repercussions
|
||||||
original_second_response = urlopen(url).read()
|
original_second_response = urlopen(url).read()
|
||||||
|
|
||||||
with vcr.use_cassette(testfile, record_mode="once"):
|
with vcr.use_cassette(testfile, record_mode=vcr.mode.ONCE):
|
||||||
# make the same request again
|
# make the same request again
|
||||||
assert urlopen(url).read() == original_first_response
|
assert urlopen(url).read() == original_first_response
|
||||||
assert urlopen(url).read() == original_second_response
|
assert urlopen(url).read() == original_second_response
|
||||||
@@ -99,16 +99,16 @@ def test_new_episodes_record_mode_two_times(tmpdir, httpbin):
|
|||||||
def test_all_record_mode(tmpdir, httpbin):
|
def test_all_record_mode(tmpdir, httpbin):
|
||||||
testfile = str(tmpdir.join("recordmode.yml"))
|
testfile = str(tmpdir.join("recordmode.yml"))
|
||||||
|
|
||||||
with vcr.use_cassette(testfile, record_mode="all"):
|
with vcr.use_cassette(testfile, record_mode=vcr.mode.ALL):
|
||||||
# cassette file doesn't exist, so create.
|
# cassette file doesn't exist, so create.
|
||||||
urlopen(httpbin.url).read()
|
urlopen(httpbin.url).read()
|
||||||
|
|
||||||
with vcr.use_cassette(testfile, record_mode="all") as cass:
|
with vcr.use_cassette(testfile, record_mode=vcr.mode.ALL) as cass:
|
||||||
# make the same request again
|
# make the same request again
|
||||||
urlopen(httpbin.url).read()
|
urlopen(httpbin.url).read()
|
||||||
|
|
||||||
# in the "all" record mode, we can add more requests to
|
# in the "all" record mode, we can add more requests to
|
||||||
# a cassette without repurcussions.
|
# a cassette without repercussions.
|
||||||
urlopen(httpbin.url + "/get").read()
|
urlopen(httpbin.url + "/get").read()
|
||||||
|
|
||||||
# The cassette was never actually played, even though it existed.
|
# The cassette was never actually played, even though it existed.
|
||||||
@@ -121,7 +121,7 @@ def test_none_record_mode(tmpdir, httpbin):
|
|||||||
# Cassette file doesn't exist, yet we are trying to make a request.
|
# Cassette file doesn't exist, yet we are trying to make a request.
|
||||||
# raise hell.
|
# raise hell.
|
||||||
testfile = str(tmpdir.join("recordmode.yml"))
|
testfile = str(tmpdir.join("recordmode.yml"))
|
||||||
with vcr.use_cassette(testfile, record_mode="none"):
|
with vcr.use_cassette(testfile, record_mode=vcr.mode.NONE):
|
||||||
with pytest.raises(Exception):
|
with pytest.raises(Exception):
|
||||||
urlopen(httpbin.url).read()
|
urlopen(httpbin.url).read()
|
||||||
|
|
||||||
@@ -130,11 +130,11 @@ def test_none_record_mode_with_existing_cassette(tmpdir, httpbin):
|
|||||||
# create a cassette file
|
# create a cassette file
|
||||||
testfile = str(tmpdir.join("recordmode.yml"))
|
testfile = str(tmpdir.join("recordmode.yml"))
|
||||||
|
|
||||||
with vcr.use_cassette(testfile, record_mode="all"):
|
with vcr.use_cassette(testfile, record_mode=vcr.mode.ALL):
|
||||||
urlopen(httpbin.url).read()
|
urlopen(httpbin.url).read()
|
||||||
|
|
||||||
# play from cassette file
|
# play from cassette file
|
||||||
with vcr.use_cassette(testfile, record_mode="none") as cass:
|
with vcr.use_cassette(testfile, record_mode=vcr.mode.NONE) as cass:
|
||||||
urlopen(httpbin.url).read()
|
urlopen(httpbin.url).read()
|
||||||
assert cass.play_count == 1
|
assert cass.play_count == 1
|
||||||
# but if I try to hit the net, raise an exception.
|
# but if I try to hit the net, raise an exception.
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ from vcr.persisters.filesystem import FilesystemPersister
|
|||||||
|
|
||||||
class CustomFilesystemPersister(object):
|
class CustomFilesystemPersister(object):
|
||||||
"""Behaves just like default FilesystemPersister but adds .test extension
|
"""Behaves just like default FilesystemPersister but adds .test extension
|
||||||
to the cassette file"""
|
to the cassette file"""
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def load_cassette(cassette_path, serializer):
|
def load_cassette(cassette_path, serializer):
|
||||||
@@ -30,7 +30,7 @@ def test_save_cassette_with_custom_persister(tmpdir, httpbin):
|
|||||||
my_vcr = vcr.VCR()
|
my_vcr = vcr.VCR()
|
||||||
my_vcr.register_persister(CustomFilesystemPersister)
|
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")))
|
assert not os.path.exists(str(tmpdir.join("nonexistent")))
|
||||||
|
|
||||||
# Run VCR to create dir and cassette file using new save_cassette callback
|
# 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"""
|
"""Test requests' interaction with vcr"""
|
||||||
import platform
|
|
||||||
import pytest
|
import pytest
|
||||||
import sys
|
|
||||||
import vcr
|
import vcr
|
||||||
from assertions import assert_cassette_empty, assert_is_json
|
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("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):
|
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."""
|
"""Ensure that we can send chunked binary without breaking while trying to concatenate bytes with str."""
|
||||||
data1 = iter([b"data", b"to", b"send"])
|
data1 = iter([b"data", b"to", b"send"])
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ def test_original_decoded_response_is_not_modified(tmpdir, httpbin):
|
|||||||
inside = conn.getresponse()
|
inside = conn.getresponse()
|
||||||
|
|
||||||
# Assert that we do not modify the original response while appending
|
# Assert that we do not modify the original response while appending
|
||||||
# to the casssette.
|
# to the cassette.
|
||||||
assert "gzip" == inside.headers["content-encoding"]
|
assert "gzip" == inside.headers["content-encoding"]
|
||||||
|
|
||||||
# They should effectively be the same response.
|
# They should effectively be the same response.
|
||||||
|
|||||||
@@ -90,10 +90,10 @@ def test_body(get_client, tmpdir, scheme):
|
|||||||
@pytest.mark.gen_test
|
@pytest.mark.gen_test
|
||||||
def test_effective_url(get_client, scheme, tmpdir):
|
def test_effective_url(get_client, scheme, tmpdir):
|
||||||
"""Ensure that the effective_url is captured"""
|
"""Ensure that the effective_url is captured"""
|
||||||
url = scheme + "://httpbin.org/redirect-to?url=/html"
|
url = scheme + "://mockbin.org/redirect/301?url=/html"
|
||||||
with vcr.use_cassette(str(tmpdir.join("url.yaml"))):
|
with vcr.use_cassette(str(tmpdir.join("url.yaml"))):
|
||||||
effective_url = (yield get(get_client(), url)).effective_url
|
effective_url = (yield get(get_client(), url)).effective_url
|
||||||
assert effective_url == scheme + "://httpbin.org/html"
|
assert effective_url == scheme + "://mockbin.org/redirect/301/0"
|
||||||
|
|
||||||
with vcr.use_cassette(str(tmpdir.join("url.yaml"))) as cass:
|
with vcr.use_cassette(str(tmpdir.join("url.yaml"))) as cass:
|
||||||
assert effective_url == (yield get(get_client(), url)).effective_url
|
assert effective_url == (yield get(get_client(), url)).effective_url
|
||||||
@@ -156,7 +156,7 @@ def test_post(get_client, tmpdir, scheme):
|
|||||||
@pytest.mark.gen_test
|
@pytest.mark.gen_test
|
||||||
def test_redirects(get_client, tmpdir, scheme):
|
def test_redirects(get_client, tmpdir, scheme):
|
||||||
"""Ensure that we can handle redirects"""
|
"""Ensure that we can handle redirects"""
|
||||||
url = scheme + "://httpbin.org/redirect-to?url=bytes/1024"
|
url = scheme + "://mockbin.org/redirect/301?url=bytes/1024"
|
||||||
with vcr.use_cassette(str(tmpdir.join("requests.yaml"))):
|
with vcr.use_cassette(str(tmpdir.join("requests.yaml"))):
|
||||||
content = (yield get(get_client(), url)).body
|
content = (yield get(get_client(), url)).body
|
||||||
|
|
||||||
|
|||||||
@@ -56,12 +56,13 @@ def test_response_headers(httpbin_both, tmpdir):
|
|||||||
assert sorted(open1) == sorted(open2)
|
assert sorted(open1) == sorted(open2)
|
||||||
|
|
||||||
|
|
||||||
def test_effective_url(httpbin_both, tmpdir):
|
def test_effective_url(tmpdir):
|
||||||
"""Ensure that the effective_url is captured"""
|
"""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"))):
|
with vcr.use_cassette(str(tmpdir.join("headers.yaml"))):
|
||||||
effective_url = urlopen_with_cafile(url).geturl()
|
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"))):
|
with vcr.use_cassette(str(tmpdir.join("headers.yaml"))):
|
||||||
assert effective_url == urlopen_with_cafile(url).geturl()
|
assert effective_url == urlopen_with_cafile(url).geturl()
|
||||||
|
|||||||
@@ -94,9 +94,10 @@ def test_post(tmpdir, httpbin_both, verify_pool_mgr):
|
|||||||
assert req1 == req2
|
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"""
|
"""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"))):
|
with vcr.use_cassette(str(tmpdir.join("verify_pool_mgr.yaml"))):
|
||||||
content = verify_pool_mgr.request("GET", url).data
|
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
|
assert content == verify_pool_mgr.request("GET", url).data
|
||||||
# Ensure that we've now cached *two* responses. One for the redirect
|
# Ensure that we've now cached *two* responses. One for the redirect
|
||||||
# and one for the final fetch
|
# 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):
|
def test_cross_scheme(tmpdir, httpbin, httpbin_secure, verify_pool_mgr):
|
||||||
|
|||||||
@@ -137,6 +137,31 @@ def test_cassette_all_played():
|
|||||||
assert a.all_played
|
assert a.all_played
|
||||||
|
|
||||||
|
|
||||||
|
@mock.patch("vcr.cassette.requests_match", _mock_requests_match)
|
||||||
|
def test_cassette_allow_playback_repeats():
|
||||||
|
a = Cassette("test", allow_playback_repeats=True)
|
||||||
|
a.append("foo", "bar")
|
||||||
|
a.append("other", "resp")
|
||||||
|
for x in range(10):
|
||||||
|
assert a.play_response("foo") == "bar"
|
||||||
|
assert a.play_count == 10
|
||||||
|
assert a.all_played is False
|
||||||
|
assert a.play_response("other") == "resp"
|
||||||
|
assert a.play_count == 11
|
||||||
|
assert a.all_played
|
||||||
|
|
||||||
|
a.allow_playback_repeats = False
|
||||||
|
with pytest.raises(UnhandledHTTPRequestError) as e:
|
||||||
|
a.play_response("foo")
|
||||||
|
assert str(e.value) == "\"The cassette ('test') doesn't contain the request ('foo') asked for\""
|
||||||
|
a.rewind()
|
||||||
|
assert a.all_played is False
|
||||||
|
assert a.play_response("foo") == "bar"
|
||||||
|
assert a.all_played is False
|
||||||
|
assert a.play_response("other") == "resp"
|
||||||
|
assert a.all_played
|
||||||
|
|
||||||
|
|
||||||
@mock.patch("vcr.cassette.requests_match", _mock_requests_match)
|
@mock.patch("vcr.cassette.requests_match", _mock_requests_match)
|
||||||
def test_cassette_rewound():
|
def test_cassette_rewound():
|
||||||
a = Cassette("test")
|
a = Cassette("test")
|
||||||
@@ -183,7 +208,7 @@ def test_nesting_cassette_context_managers(*args):
|
|||||||
)
|
)
|
||||||
assert_get_response_body_is("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 Cassette.use(path="test") as second_cassette:
|
||||||
with mock.patch.object(second_cassette, "play_response", return_value=second_response):
|
with mock.patch.object(second_cassette, "play_response", return_value=second_response):
|
||||||
assert_get_response_body_is("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():
|
def test_use_as_decorator_on_coroutine():
|
||||||
original_http_connetion = httplib.HTTPConnection
|
original_http_connection = httplib.HTTPConnection
|
||||||
|
|
||||||
@Cassette.use(inject=True)
|
@Cassette.use(inject=True)
|
||||||
def test_function(cassette):
|
def test_function(cassette):
|
||||||
assert httplib.HTTPConnection.cassette is 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
|
value = yield 1
|
||||||
assert value == 1
|
assert value == 1
|
||||||
assert httplib.HTTPConnection.cassette is 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 2
|
value = yield 2
|
||||||
assert value == 2
|
assert value == 2
|
||||||
|
|
||||||
@@ -308,15 +333,15 @@ def test_use_as_decorator_on_coroutine():
|
|||||||
|
|
||||||
|
|
||||||
def test_use_as_decorator_on_generator():
|
def test_use_as_decorator_on_generator():
|
||||||
original_http_connetion = httplib.HTTPConnection
|
original_http_connection = httplib.HTTPConnection
|
||||||
|
|
||||||
@Cassette.use(inject=True)
|
@Cassette.use(inject=True)
|
||||||
def test_function(cassette):
|
def test_function(cassette):
|
||||||
assert httplib.HTTPConnection.cassette is 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
|
yield 1
|
||||||
assert httplib.HTTPConnection.cassette is cassette
|
assert httplib.HTTPConnection.cassette is cassette
|
||||||
assert httplib.HTTPConnection is not original_http_connetion
|
assert httplib.HTTPConnection is not original_http_connection
|
||||||
yield 2
|
yield 2
|
||||||
|
|
||||||
assert list(test_function()) == [1, 2]
|
assert list(test_function()) == [1, 2]
|
||||||
|
|||||||
@@ -220,6 +220,49 @@ def test_remove_all_json_post_data_parameters():
|
|||||||
assert request.body == b"{}"
|
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():
|
def test_decode_response_uncompressed():
|
||||||
recorded_response = {
|
recorded_response = {
|
||||||
"status": {"message": "OK", "code": 200},
|
"status": {"message": "OK", "code": 200},
|
||||||
|
|||||||
@@ -44,4 +44,4 @@ def test_try_migrate_with_invalid_or_new_cassettes(tmpdir):
|
|||||||
for file_path in files:
|
for file_path in files:
|
||||||
shutil.copy(file_path, cassette)
|
shutil.copy(file_path, cassette)
|
||||||
assert not vcr.migration.try_migrate(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 unittest import mock
|
||||||
|
|
||||||
|
from vcr import mode
|
||||||
from vcr.stubs import VCRHTTPSConnection
|
from vcr.stubs import VCRHTTPSConnection
|
||||||
from vcr.cassette import Cassette
|
from vcr.cassette import Cassette
|
||||||
|
|
||||||
|
|
||||||
class TestVCRConnection:
|
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 = VCRHTTPSConnection("www.examplehost.com")
|
||||||
vcr_connection.ssl_version = "example_ssl_version"
|
vcr_connection.ssl_version = "example_ssl_version"
|
||||||
assert vcr_connection.real_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)
|
@mock.patch("vcr.cassette.Cassette.can_play_response_for", return_value=False)
|
||||||
def testing_connect(*args):
|
def testing_connect(*args):
|
||||||
vcr_connection = VCRHTTPSConnection("www.google.com")
|
vcr_connection = VCRHTTPSConnection("www.google.com")
|
||||||
vcr_connection.cassette = Cassette("test", record_mode="all")
|
vcr_connection.cassette = Cassette("test", record_mode=mode.ALL)
|
||||||
vcr_connection.real_connection.connect()
|
vcr_connection.real_connection.connect()
|
||||||
assert vcr_connection.real_connection.sock is not None
|
assert vcr_connection.real_connection.sock is not None
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import os
|
|||||||
import pytest
|
import pytest
|
||||||
import http.client as httplib
|
import http.client as httplib
|
||||||
|
|
||||||
from vcr import VCR, use_cassette
|
from vcr import VCR, mode, use_cassette
|
||||||
from vcr.request import Request
|
from vcr.request import Request
|
||||||
from vcr.stubs import VCRHTTPSConnection
|
from vcr.stubs import VCRHTTPSConnection
|
||||||
from vcr.patch import _HTTPConnection, force_reset
|
from vcr.patch import _HTTPConnection, force_reset
|
||||||
@@ -31,7 +31,7 @@ def test_vcr_use_cassette():
|
|||||||
function()
|
function()
|
||||||
assert mock_cassette_load.call_args[1]["record_mode"] == test_vcr.record_mode
|
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.
|
# those on the vcr.
|
||||||
new_record_mode = mock.Mock()
|
new_record_mode = mock.Mock()
|
||||||
|
|
||||||
@@ -188,11 +188,11 @@ def test_custom_patchers():
|
|||||||
def test_inject_cassette():
|
def test_inject_cassette():
|
||||||
vcr = VCR(inject_cassette=True)
|
vcr = VCR(inject_cassette=True)
|
||||||
|
|
||||||
@vcr.use_cassette("test", record_mode="once")
|
@vcr.use_cassette("test", record_mode=mode.ONCE)
|
||||||
def with_cassette_injected(cassette):
|
def with_cassette_injected(cassette):
|
||||||
assert cassette.record_mode == "once"
|
assert cassette.record_mode == mode.ONCE
|
||||||
|
|
||||||
@vcr.use_cassette("test", record_mode="once", inject_cassette=False)
|
@vcr.use_cassette("test", record_mode=mode.ONCE, inject_cassette=False)
|
||||||
def without_cassette_injected():
|
def without_cassette_injected():
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -201,7 +201,7 @@ def test_inject_cassette():
|
|||||||
|
|
||||||
|
|
||||||
def test_with_current_defaults():
|
def test_with_current_defaults():
|
||||||
vcr = VCR(inject_cassette=True, record_mode="once")
|
vcr = VCR(inject_cassette=True, record_mode=mode.ONCE)
|
||||||
|
|
||||||
@vcr.use_cassette("test", with_current_defaults=False)
|
@vcr.use_cassette("test", with_current_defaults=False)
|
||||||
def changing_defaults(cassette, checks):
|
def changing_defaults(cassette, checks):
|
||||||
@@ -212,10 +212,10 @@ def test_with_current_defaults():
|
|||||||
checks(cassette)
|
checks(cassette)
|
||||||
|
|
||||||
def assert_record_mode_once(cassette):
|
def assert_record_mode_once(cassette):
|
||||||
assert cassette.record_mode == "once"
|
assert cassette.record_mode == mode.ONCE
|
||||||
|
|
||||||
def assert_record_mode_all(cassette):
|
def assert_record_mode_all(cassette):
|
||||||
assert cassette.record_mode == "all"
|
assert cassette.record_mode == mode.ALL
|
||||||
|
|
||||||
changing_defaults(assert_record_mode_once)
|
changing_defaults(assert_record_mode_once)
|
||||||
current_defaults(assert_record_mode_once)
|
current_defaults(assert_record_mode_once)
|
||||||
@@ -226,7 +226,7 @@ def test_with_current_defaults():
|
|||||||
|
|
||||||
|
|
||||||
def test_cassette_library_dir_with_decoration_and_no_explicit_path():
|
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 = VCR(inject_cassette=True, cassette_library_dir=library_dir)
|
||||||
|
|
||||||
@vcr.use_cassette()
|
@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():
|
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 = VCR(inject_cassette=True, cassette_library_dir=library_dir)
|
||||||
|
|
||||||
@vcr.use_cassette(path="custom_name")
|
@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():
|
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 = VCR(inject_cassette=True, cassette_library_dir=library_dir)
|
||||||
|
|
||||||
@vcr.use_cassette(path=os.path.join(library_dir, "custom_name"))
|
@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():
|
def test_cassette_library_dir_with_path_transformer():
|
||||||
library_dir = "/libary_dir"
|
library_dir = "/library_dir"
|
||||||
vcr = VCR(
|
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"
|
||||||
)
|
)
|
||||||
|
|||||||
40
tox.ini
40
tox.ini
@@ -1,13 +1,22 @@
|
|||||||
[tox]
|
[tox]
|
||||||
skip_missing_interpreters=true
|
skip_missing_interpreters=true
|
||||||
envlist =
|
envlist =
|
||||||
cov-clean,
|
cov-clean,
|
||||||
lint,
|
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},
|
{pypy3}-{requests,httplib2,urllib3,tornado4,boto3},
|
||||||
|
{py310}-httpx019,
|
||||||
cov-report
|
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
|
# Coverage environment tasks: cov-clean and cov-report
|
||||||
# https://pytest-cov.readthedocs.io/en/latest/tox.html
|
# https://pytest-cov.readthedocs.io/en/latest/tox.html
|
||||||
[testenv:cov-clean]
|
[testenv:cov-clean]
|
||||||
@@ -18,7 +27,7 @@ commands = coverage erase
|
|||||||
[testenv:cov-report]
|
[testenv:cov-report]
|
||||||
deps = coverage
|
deps = coverage
|
||||||
skip_install=true
|
skip_install=true
|
||||||
commands =
|
commands =
|
||||||
coverage html
|
coverage html
|
||||||
coverage report --fail-under=90
|
coverage report --fail-under=90
|
||||||
|
|
||||||
@@ -30,9 +39,10 @@ commands =
|
|||||||
flake8 --version
|
flake8 --version
|
||||||
flake8 --exclude=./docs/conf.py,./.tox/
|
flake8 --exclude=./docs/conf.py,./.tox/
|
||||||
pyflakes ./docs/conf.py
|
pyflakes ./docs/conf.py
|
||||||
deps =
|
deps =
|
||||||
flake8
|
flake8
|
||||||
black
|
black
|
||||||
|
basepython = python3.7
|
||||||
|
|
||||||
[testenv:docs]
|
[testenv:docs]
|
||||||
# Running sphinx from inside the "docs" directory
|
# Running sphinx from inside the "docs" directory
|
||||||
@@ -62,26 +72,30 @@ usedevelop=true
|
|||||||
commands =
|
commands =
|
||||||
./runtests.sh --cov=./vcr --cov-branch --cov-report=xml --cov-append {posargs}
|
./runtests.sh --cov=./vcr --cov-branch --cov-report=xml --cov-append {posargs}
|
||||||
deps =
|
deps =
|
||||||
Flask
|
Werkzeug==2.0.3
|
||||||
pytest
|
pytest
|
||||||
pytest-httpbin
|
git+https://github.com/immerrr/pytest-httpbin@fix-redirect-location-scheme-for-secure-server
|
||||||
pytest-cov
|
pytest-cov
|
||||||
PyYAML
|
PyYAML
|
||||||
ipaddress
|
ipaddress
|
||||||
requests: requests>=2.22.0
|
requests: requests>=2.22.0
|
||||||
httplib2: httplib2
|
httplib2: httplib2
|
||||||
urllib3: urllib3
|
urllib3: urllib3
|
||||||
{py35,py36}-tornado4: tornado>=4,<5
|
|
||||||
{py35,py36}-tornado4: pytest-tornado
|
|
||||||
{py35,py36}-tornado4: pycurl
|
|
||||||
boto3: boto3
|
boto3: boto3
|
||||||
boto3: urllib3
|
boto3: urllib3
|
||||||
aiohttp: aiohttp
|
aiohttp: aiohttp
|
||||||
aiohttp: pytest-asyncio
|
aiohttp: pytest-asyncio
|
||||||
aiohttp: pytest-aiohttp
|
aiohttp: pytest-aiohttp
|
||||||
depends =
|
httpx: httpx
|
||||||
lint,{py35,py36,py37,py38,pypy3}-{requests,httplib2,urllib3,tornado4,boto3},{py35,py36,py37,py38}-{aiohttp}: cov-clean
|
{py37,py38,py39,py310}-{httpx}: httpx
|
||||||
cov-report: lint,{py35,py36,py37,py38,pypy3}-{requests,httplib2,urllib3,tornado4,boto3},{py35,py36,py37,py38}-{aiohttp}
|
{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 =
|
passenv =
|
||||||
AWS_ACCESS_KEY_ID
|
AWS_ACCESS_KEY_ID
|
||||||
AWS_DEFAULT_REGION
|
AWS_DEFAULT_REGION
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import logging
|
import logging
|
||||||
from .config import VCR
|
from .config import VCR
|
||||||
from logging import NullHandler
|
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.1"
|
||||||
|
|
||||||
logging.getLogger(__name__).addHandler(NullHandler())
|
logging.getLogger(__name__).addHandler(NullHandler())
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ from .serializers import yamlserializer
|
|||||||
from .persisters.filesystem import FilesystemPersister
|
from .persisters.filesystem import FilesystemPersister
|
||||||
from .util import partition_dict
|
from .util import partition_dict
|
||||||
from ._handle_coroutine import handle_coroutine
|
from ._handle_coroutine import handle_coroutine
|
||||||
|
from .record_mode import RecordMode
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from asyncio import iscoroutinefunction
|
from asyncio import iscoroutinefunction
|
||||||
@@ -175,12 +176,13 @@ class Cassette:
|
|||||||
path,
|
path,
|
||||||
serializer=None,
|
serializer=None,
|
||||||
persister=None,
|
persister=None,
|
||||||
record_mode="once",
|
record_mode=RecordMode.ONCE,
|
||||||
match_on=(uri, method),
|
match_on=(uri, method),
|
||||||
before_record_request=None,
|
before_record_request=None,
|
||||||
before_record_response=None,
|
before_record_response=None,
|
||||||
custom_patches=(),
|
custom_patches=(),
|
||||||
inject=False,
|
inject=False,
|
||||||
|
allow_playback_repeats=False,
|
||||||
):
|
):
|
||||||
self._persister = persister or FilesystemPersister
|
self._persister = persister or FilesystemPersister
|
||||||
self._path = path
|
self._path = path
|
||||||
@@ -192,6 +194,7 @@ class Cassette:
|
|||||||
self.inject = inject
|
self.inject = inject
|
||||||
self.record_mode = record_mode
|
self.record_mode = record_mode
|
||||||
self.custom_patches = custom_patches
|
self.custom_patches = custom_patches
|
||||||
|
self.allow_playback_repeats = allow_playback_repeats
|
||||||
|
|
||||||
# self.data is the list of (req, resp) tuples
|
# self.data is the list of (req, resp) tuples
|
||||||
self.data = []
|
self.data = []
|
||||||
@@ -206,7 +209,7 @@ class Cassette:
|
|||||||
@property
|
@property
|
||||||
def all_played(self):
|
def all_played(self):
|
||||||
"""Returns True if all responses have been played, False otherwise."""
|
"""Returns True if all responses have been played, False otherwise."""
|
||||||
return self.play_count == len(self)
|
return len(self.play_counts.values()) == len(self)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def requests(self):
|
def requests(self):
|
||||||
@@ -218,7 +221,7 @@ class Cassette:
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def write_protected(self):
|
def write_protected(self):
|
||||||
return self.rewound and self.record_mode == "once" or self.record_mode == "none"
|
return self.rewound and self.record_mode == RecordMode.ONCE or self.record_mode == RecordMode.NONE
|
||||||
|
|
||||||
def append(self, request, response):
|
def append(self, request, response):
|
||||||
"""Add a request, response pair to this cassette"""
|
"""Add a request, response pair to this cassette"""
|
||||||
@@ -250,7 +253,7 @@ class Cassette:
|
|||||||
|
|
||||||
def can_play_response_for(self, request):
|
def can_play_response_for(self, request):
|
||||||
request = self._before_record_request(request)
|
request = self._before_record_request(request)
|
||||||
return request and request in self and self.record_mode != "all" and self.rewound
|
return request and request in self and self.record_mode != RecordMode.ALL and self.rewound
|
||||||
|
|
||||||
def play_response(self, request):
|
def play_response(self, request):
|
||||||
"""
|
"""
|
||||||
@@ -258,7 +261,7 @@ class Cassette:
|
|||||||
hasn't been played back before, and mark it as played
|
hasn't been played back before, and mark it as played
|
||||||
"""
|
"""
|
||||||
for index, response in self._responses(request):
|
for index, response in self._responses(request):
|
||||||
if self.play_counts[index] == 0:
|
if self.play_counts[index] == 0 or self.allow_playback_repeats:
|
||||||
self.play_counts[index] += 1
|
self.play_counts[index] += 1
|
||||||
return response
|
return response
|
||||||
# The cassette doesn't contain the request asked for.
|
# The cassette doesn't contain the request asked for.
|
||||||
@@ -348,6 +351,6 @@ class Cassette:
|
|||||||
def __contains__(self, request):
|
def __contains__(self, request):
|
||||||
"""Return whether or not a request has been stored"""
|
"""Return whether or not a request has been stored"""
|
||||||
for index, response in self._responses(request):
|
for index, response in self._responses(request):
|
||||||
if self.play_counts[index] == 0:
|
if self.play_counts[index] == 0 or self.allow_playback_repeats:
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ from .cassette import Cassette
|
|||||||
from .serializers import yamlserializer, jsonserializer
|
from .serializers import yamlserializer, jsonserializer
|
||||||
from .persisters.filesystem import FilesystemPersister
|
from .persisters.filesystem import FilesystemPersister
|
||||||
from .util import compose, auto_decorate
|
from .util import compose, auto_decorate
|
||||||
|
from .record_mode import RecordMode
|
||||||
from . import matchers
|
from . import matchers
|
||||||
from . import filters
|
from . import filters
|
||||||
|
|
||||||
@@ -37,7 +38,7 @@ class VCR:
|
|||||||
custom_patches=(),
|
custom_patches=(),
|
||||||
filter_query_parameters=(),
|
filter_query_parameters=(),
|
||||||
ignore_hosts=(),
|
ignore_hosts=(),
|
||||||
record_mode="once",
|
record_mode=RecordMode.ONCE,
|
||||||
ignore_localhost=False,
|
ignore_localhost=False,
|
||||||
filter_headers=(),
|
filter_headers=(),
|
||||||
before_record_response=None,
|
before_record_response=None,
|
||||||
@@ -148,6 +149,7 @@ class VCR:
|
|||||||
"inject": kwargs.get("inject_cassette", self.inject_cassette),
|
"inject": kwargs.get("inject_cassette", self.inject_cassette),
|
||||||
"path_transformer": path_transformer,
|
"path_transformer": path_transformer,
|
||||||
"func_path_generator": func_path_generator,
|
"func_path_generator": func_path_generator,
|
||||||
|
"allow_playback_repeats": kwargs.get("allow_playback_repeats", False),
|
||||||
}
|
}
|
||||||
path = kwargs.get("path")
|
path = kwargs.get("path")
|
||||||
if path:
|
if path:
|
||||||
|
|||||||
@@ -84,7 +84,17 @@ def replace_post_data_parameters(request, replacements):
|
|||||||
|
|
||||||
replacements = dict(replacements)
|
replacements = dict(replacements)
|
||||||
if request.method == "POST" and not isinstance(request.body, BytesIO):
|
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"))
|
json_data = json.loads(request.body.decode("utf-8"))
|
||||||
for k, rv in replacements.items():
|
for k, rv in replacements.items():
|
||||||
if k in json_data:
|
if k in json_data:
|
||||||
|
|||||||
25
vcr/patch.py
25
vcr/patch.py
@@ -94,6 +94,15 @@ else:
|
|||||||
_AiohttpClientSessionRequest = aiohttp.client.ClientSession._request
|
_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:
|
class CassettePatcherBuilder:
|
||||||
def _build_patchers_from_mock_triples_decorator(function):
|
def _build_patchers_from_mock_triples_decorator(function):
|
||||||
@functools.wraps(function)
|
@functools.wraps(function)
|
||||||
@@ -116,6 +125,7 @@ class CassettePatcherBuilder:
|
|||||||
self._boto(),
|
self._boto(),
|
||||||
self._tornado(),
|
self._tornado(),
|
||||||
self._aiohttp(),
|
self._aiohttp(),
|
||||||
|
self._httpx(),
|
||||||
self._build_patchers_from_mock_triples(self._cassette.custom_patches),
|
self._build_patchers_from_mock_triples(self._cassette.custom_patches),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -313,6 +323,21 @@ class CassettePatcherBuilder:
|
|||||||
new_request = vcr_request(self._cassette, _AiohttpClientSessionRequest)
|
new_request = vcr_request(self._cassette, _AiohttpClientSessionRequest)
|
||||||
yield client.ClientSession, "_request", new_request
|
yield client.ClientSession, "_request", new_request
|
||||||
|
|
||||||
|
@_build_patchers_from_mock_triples_decorator
|
||||||
|
def _httpx(self):
|
||||||
|
try:
|
||||||
|
import httpx
|
||||||
|
except ImportError: # pragma: no cover
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
from .stubs.httpx_stubs import async_vcr_send, sync_vcr_send
|
||||||
|
|
||||||
|
new_async_client_send = async_vcr_send(self._cassette, _HttpxAsyncClient_send)
|
||||||
|
yield httpx.AsyncClient, "send", new_async_client_send
|
||||||
|
|
||||||
|
new_sync_client_send = sync_vcr_send(self._cassette, _HttpxSyncClient_send)
|
||||||
|
yield httpx.Client, "send", new_sync_client_send
|
||||||
|
|
||||||
def _urllib3_patchers(self, cpool, stubs):
|
def _urllib3_patchers(self, cpool, stubs):
|
||||||
http_connection_remover = ConnectionRemover(
|
http_connection_remover = ConnectionRemover(
|
||||||
self._get_cassette_subclass(stubs.VCRRequestsHTTPConnection)
|
self._get_cassette_subclass(stubs.VCRRequestsHTTPConnection)
|
||||||
|
|||||||
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:
|
try:
|
||||||
return json.dumps(cassette_dict, indent=4)
|
return json.dumps(cassette_dict, indent=4) + "\n"
|
||||||
except UnicodeDecodeError as original: # py2
|
except UnicodeDecodeError as original: # py2
|
||||||
raise UnicodeDecodeError(
|
raise UnicodeDecodeError(
|
||||||
original.encoding,
|
original.encoding,
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ class VCRHTTPResponse(HTTPResponse):
|
|||||||
def closed(self):
|
def closed(self):
|
||||||
# in python3, I can't change the value of self.closed. So I'
|
# in python3, I can't change the value of self.closed. So I'
|
||||||
# twiddling self._closed and using this property to shadow the real
|
# twiddling self._closed and using this property to shadow the real
|
||||||
# self.closed from the superclas
|
# self.closed from the superclass
|
||||||
return self._closed
|
return self._closed
|
||||||
|
|
||||||
def read(self, *args, **kwargs):
|
def read(self, *args, **kwargs):
|
||||||
@@ -314,7 +314,7 @@ class VCRConnection:
|
|||||||
def __setattr__(self, name, value):
|
def __setattr__(self, name, value):
|
||||||
"""
|
"""
|
||||||
We need to define this because any attributes that are set on the
|
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,
|
For example, urllib3 will set certain attributes on the connection,
|
||||||
such as 'ssl_version'. These attributes need to get set on the real
|
such as 'ssl_version'. These attributes need to get set on the real
|
||||||
|
|||||||
@@ -5,9 +5,14 @@ import logging
|
|||||||
import json
|
import json
|
||||||
|
|
||||||
from aiohttp import ClientConnectionError, ClientResponse, RequestInfo, streams
|
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 yarl import URL
|
||||||
|
|
||||||
|
from vcr.errors import CannotOverwriteExistingCassetteException
|
||||||
from vcr.request import Request
|
from vcr.request import Request
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
@@ -59,15 +64,27 @@ def build_response(vcr_request, vcr_response, history):
|
|||||||
request_info = RequestInfo(
|
request_info = RequestInfo(
|
||||||
url=URL(vcr_request.url),
|
url=URL(vcr_request.url),
|
||||||
method=vcr_request.method,
|
method=vcr_request.method,
|
||||||
headers=CIMultiDictProxy(CIMultiDict(vcr_request.headers)),
|
headers=_deserialize_headers(vcr_request.headers),
|
||||||
real_url=URL(vcr_request.url),
|
real_url=URL(vcr_request.url),
|
||||||
)
|
)
|
||||||
response = MockClientResponse(vcr_request.method, URL(vcr_response.get("url")), request_info=request_info)
|
response = MockClientResponse(vcr_request.method, URL(vcr_response.get("url")), request_info=request_info)
|
||||||
response.status = vcr_response["status"]["code"]
|
response.status = vcr_response["status"]["code"]
|
||||||
response._body = vcr_response["body"].get("string", b"")
|
response._body = vcr_response["body"].get("string", b"")
|
||||||
response.reason = vcr_response["status"]["message"]
|
response.reason = vcr_response["status"]["message"]
|
||||||
response._headers = CIMultiDictProxy(CIMultiDict(vcr_response["headers"]))
|
response._headers = _deserialize_headers(vcr_response["headers"])
|
||||||
response._history = tuple(history)
|
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()
|
response.close()
|
||||||
return response
|
return response
|
||||||
@@ -75,31 +92,54 @@ def build_response(vcr_request, vcr_response, history):
|
|||||||
|
|
||||||
def _serialize_headers(headers):
|
def _serialize_headers(headers):
|
||||||
"""Serialize CIMultiDictProxy to a pickle-able dict because proxy
|
"""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
|
# Mark strings as keys so 'istr' types don't show up in
|
||||||
# the cassettes as comments.
|
# 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 = []
|
history = []
|
||||||
|
allow_redirects = kwargs.get("allow_redirects", True)
|
||||||
vcr_response = cassette.play_response(vcr_request)
|
vcr_response = cassette.play_response(vcr_request)
|
||||||
response = build_response(vcr_request, vcr_response, history)
|
response = build_response(vcr_request, vcr_response, history)
|
||||||
|
|
||||||
# If we're following redirects, continue playing until we reach
|
# If we're following redirects, continue playing until we reach
|
||||||
# our final destination.
|
# our final destination.
|
||||||
while 300 <= response.status <= 399:
|
while allow_redirects and 300 <= response.status <= 399:
|
||||||
next_url = URL(response.url).with_path(response.headers["location"])
|
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
|
# 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
|
# 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
|
# 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).
|
# 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 = 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
|
# Tack on the response we saw from the redirect into the history
|
||||||
# list that is added on to the final response.
|
# list that is added on to the final response.
|
||||||
@@ -132,19 +172,19 @@ async def record_response(cassette, vcr_request, response):
|
|||||||
|
|
||||||
async def record_responses(cassette, vcr_request, response):
|
async def record_responses(cassette, vcr_request, response):
|
||||||
"""Because aiohttp follows redirects by default, we must support
|
"""Because aiohttp follows redirects by default, we must support
|
||||||
them by default. This method is used to write individual
|
them by default. This method is used to write individual
|
||||||
request-response chains that were implicitly followed to get
|
request-response chains that were implicitly followed to get
|
||||||
to the final destination.
|
to the final destination.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
for past_response in response.history:
|
for i, past_response in enumerate(response.history):
|
||||||
aiohttp_request = past_response.request_info
|
aiohttp_request = past_response.request_info
|
||||||
|
|
||||||
# No data because it's following a redirect.
|
|
||||||
past_request = Request(
|
past_request = Request(
|
||||||
aiohttp_request.method,
|
aiohttp_request.method,
|
||||||
str(aiohttp_request.url),
|
str(aiohttp_request.url),
|
||||||
None,
|
# Record body of first request, rest are following a redirect.
|
||||||
|
None if i else vcr_request.body,
|
||||||
_serialize_headers(aiohttp_request.headers),
|
_serialize_headers(aiohttp_request.headers),
|
||||||
)
|
)
|
||||||
await record_response(cassette, past_request, past_response)
|
await record_response(cassette, past_request, past_response)
|
||||||
@@ -163,6 +203,43 @@ async def record_responses(cassette, vcr_request, response):
|
|||||||
await record_response(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):
|
def vcr_request(cassette, real_request):
|
||||||
@functools.wraps(real_request)
|
@functools.wraps(real_request)
|
||||||
async def new_request(self, method, url, **kwargs):
|
async def new_request(self, method, url, **kwargs):
|
||||||
@@ -171,32 +248,29 @@ def vcr_request(cassette, real_request):
|
|||||||
headers = self._prepare_headers(headers)
|
headers = self._prepare_headers(headers)
|
||||||
data = kwargs.get("data", kwargs.get("json"))
|
data = kwargs.get("data", kwargs.get("json"))
|
||||||
params = kwargs.get("params")
|
params = kwargs.get("params")
|
||||||
|
cookies = kwargs.get("cookies")
|
||||||
|
|
||||||
if auth is not None:
|
if auth is not None:
|
||||||
headers["AUTHORIZATION"] = auth.encode()
|
headers["AUTHORIZATION"] = auth.encode()
|
||||||
|
|
||||||
request_url = URL(url)
|
request_url = URL(url) if not params else _build_url_with_params(url, params)
|
||||||
if params:
|
c_header = headers.pop(hdrs.COOKIE, None)
|
||||||
for k, v in params.items():
|
cookie_header = _build_cookie_header(self, cookies, c_header, request_url)
|
||||||
params[k] = str(v)
|
if cookie_header:
|
||||||
request_url = URL(url).with_query(params)
|
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):
|
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):
|
if cassette.write_protected and cassette.filter_request(vcr_request):
|
||||||
response = MockClientResponse(method, URL(url))
|
raise CannotOverwriteExistingCassetteException(cassette=cassette, failed_request=vcr_request)
|
||||||
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)
|
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