mirror of
https://github.com/kevin1024/vcrpy.git
synced 2025-12-09 17:15:35 +00:00
Compare commits
141 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1d100dda25 | ||
|
|
c6be705fb4 | ||
|
|
10b7f4efb3 | ||
|
|
7a6ef00f4d | ||
|
|
3bf6ac7184 | ||
|
|
983b2202ed | ||
|
|
15a6b71997 | ||
|
|
1ca708dcff | ||
|
|
f5597fa6c1 | ||
|
|
2b3247b3df | ||
|
|
d123a5e8d0 | ||
|
|
e2815fbc88 | ||
|
|
f9d4500c6e | ||
|
|
71eb624708 | ||
|
|
dc449715c1 | ||
|
|
275b9085f3 | ||
|
|
35650b141b | ||
|
|
9c8b679136 | ||
|
|
fab082eff5 | ||
|
|
ffc04f9128 | ||
|
|
4d84da1809 | ||
|
|
241b0bbd91 | ||
|
|
042e16c3e4 | ||
|
|
acef3f49bf | ||
|
|
9cfa6c5173 | ||
|
|
39a86ba3cf | ||
|
|
543c72ba51 | ||
|
|
86b114f2f5 | ||
|
|
4b06f3dba1 | ||
|
|
1c6503526b | ||
|
|
c9c05682cb | ||
|
|
39c8648aa7 | ||
|
|
dfff84d5bb | ||
|
|
40ac0de652 | ||
|
|
f3147f574b | ||
|
|
298a6933ff | ||
|
|
52da776b59 | ||
|
|
8842fb1c3a | ||
|
|
6c4ba172d8 | ||
|
|
c88f2c0dab | ||
|
|
3fd6b1c0b4 | ||
|
|
c6d87309f4 | ||
|
|
1fb9179cf9 | ||
|
|
a58e0d8830 | ||
|
|
acc101412d | ||
|
|
e60dafb8dc | ||
|
|
3ce5979acb | ||
|
|
68038d0559 | ||
|
|
f76289aa78 | ||
|
|
6252b92f50 | ||
|
|
1e3a5ac753 | ||
|
|
b1c45cd249 | ||
|
|
3a5ff1c1ce | ||
|
|
bf80673454 | ||
|
|
8028420cbb | ||
|
|
784b2dcb29 | ||
|
|
42b4a5d2fa | ||
|
|
b7f6c2fce2 | ||
|
|
6d7a842a33 | ||
|
|
db1f5b0dee | ||
|
|
c6667ac56c | ||
|
|
a093fb177d | ||
|
|
666686b542 | ||
|
|
5104b1f462 | ||
|
|
62fe272a8e | ||
|
|
f9b69d8da7 | ||
|
|
cb77cb8f69 | ||
|
|
e37fc9ab6e | ||
|
|
abbb50135f | ||
|
|
0594de9b3e | ||
|
|
53f686aa5b | ||
|
|
1677154f04 | ||
|
|
54bc6467eb | ||
|
|
c5487384ee | ||
|
|
5cf23298ac | ||
|
|
5fa7010712 | ||
|
|
f1e0241673 | ||
|
|
a3a255d606 | ||
|
|
0782382982 | ||
|
|
395d2be295 | ||
|
|
ee6e7905e9 | ||
|
|
cc4d03c62e | ||
|
|
8e13af2ee9 | ||
|
|
b522d3f0a3 | ||
|
|
d39c26b358 | ||
|
|
d76c243513 | ||
|
|
5cff354ec8 | ||
|
|
80614dbd00 | ||
|
|
356ff4122c | ||
|
|
cf765928ac | ||
|
|
73d11e80eb | ||
|
|
97de8a0fce | ||
|
|
895ae205ca | ||
|
|
f075c8b0b4 | ||
|
|
3919cb2573 | ||
|
|
bddec2e62a | ||
|
|
fa789e975b | ||
|
|
556fd0166c | ||
|
|
17c78bff9e | ||
|
|
713cb36d35 | ||
|
|
b0cb8765d5 | ||
|
|
97ad51fe6c | ||
|
|
1dd9cbde8b | ||
|
|
962284072b | ||
|
|
e9102b2bb4 | ||
|
|
957c8bd7a3 | ||
|
|
2d5f8a499e | ||
|
|
e5555a5d5b | ||
|
|
a542567e4a | ||
|
|
3168e7813e | ||
|
|
88cf01aa14 | ||
|
|
85ae012d9c | ||
|
|
db1e9e7180 | ||
|
|
dbf7a3337b | ||
|
|
dd97b02b72 | ||
|
|
e8346ad30e | ||
|
|
6a31904333 | ||
|
|
796dc8de7e | ||
|
|
ecb5d84f0f | ||
|
|
cebdd45849 | ||
|
|
8a8d46f130 | ||
|
|
954a100dfd | ||
|
|
604c0be571 | ||
|
|
0e57182207 | ||
|
|
c062c9f54c | ||
|
|
2abf1188a9 | ||
|
|
2b2935a1e7 | ||
|
|
a8545c89a5 | ||
|
|
5532c0b4cf | ||
|
|
f4467a8d6c | ||
|
|
f5fc7aac22 | ||
|
|
e8e9a4af9f | ||
|
|
7bf8f65815 | ||
|
|
defad28771 | ||
|
|
69621c67fb | ||
|
|
469a10b980 | ||
|
|
d90cea0260 | ||
|
|
c9da7a102f | ||
|
|
f4144359f6 | ||
|
|
69de388649 | ||
|
|
6446d00e27 |
2
.github/workflows/codespell.yml
vendored
2
.github/workflows/codespell.yml
vendored
@@ -17,6 +17,6 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
- name: Codespell
|
||||
uses: codespell-project/actions-codespell@v2
|
||||
|
||||
6
.github/workflows/docs.yml
vendored
6
.github/workflows/docs.yml
vendored
@@ -10,10 +10,10 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-python@v4
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.11"
|
||||
python-version: "3.12"
|
||||
|
||||
- name: Install build dependencies
|
||||
run: pip install -r docs/requirements.txt
|
||||
|
||||
49
.github/workflows/main.yml
vendored
49
.github/workflows/main.yml
vendored
@@ -5,6 +5,8 @@ on:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
schedule:
|
||||
- cron: '0 16 * * 5' # Every Friday 4pm
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
@@ -13,31 +15,58 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
python-version: ["3.8", "3.9", "3.10", "3.11", "pypy-3.8"]
|
||||
python-version:
|
||||
- "3.8"
|
||||
- "3.9"
|
||||
- "3.10"
|
||||
- "3.11"
|
||||
- "3.12"
|
||||
- "3.13"
|
||||
- "pypy-3.8"
|
||||
- "pypy-3.9"
|
||||
- "pypy-3.10"
|
||||
urllib3-requirement:
|
||||
- "urllib3>=2"
|
||||
- "urllib3<2"
|
||||
|
||||
exclude:
|
||||
- python-version: "3.8"
|
||||
urllib3-requirement: "urllib3>=2"
|
||||
- python-version: "pypy-3.8"
|
||||
urllib3-requirement: "urllib3>=2"
|
||||
- python-version: "3.9"
|
||||
urllib3-requirement: "urllib3>=2"
|
||||
- python-version: "pypy-3.9"
|
||||
urllib3-requirement: "urllib3>=2"
|
||||
- python-version: "pypy-3.10"
|
||||
urllib3-requirement: "urllib3>=2"
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3.5.2
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v4
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
cache: pip
|
||||
allow-prereleases: true
|
||||
|
||||
- name: Install project dependencies
|
||||
run: |
|
||||
pip3 install --upgrade pip
|
||||
pip3 install codecov tox tox-gh-actions
|
||||
pip install --upgrade pip setuptools
|
||||
pip install codecov '.[tests]' '${{ matrix.urllib3-requirement }}'
|
||||
pip check
|
||||
|
||||
- name: Run online tests with tox
|
||||
run: tox -- -m online
|
||||
- name: Run online tests
|
||||
run: ./runtests.sh --cov=./vcr --cov-branch --cov-report=xml --cov-append -m online
|
||||
|
||||
- name: Run offline tests with tox with no access to the Internet
|
||||
- name: Run offline tests with no access to the Internet
|
||||
run: |
|
||||
# We're using unshare to take Internet access
|
||||
# away from tox so that we'll notice whenever some new test
|
||||
# away so that we'll notice whenever some new test
|
||||
# is missing @pytest.mark.online decoration in the future
|
||||
unshare --map-root-user --net -- \
|
||||
sh -c 'ip link set lo up; tox -- -m "not online"'
|
||||
sh -c 'ip link set lo up; ./runtests.sh --cov=./vcr --cov-branch --cov-report=xml --cov-append -m "not online"'
|
||||
|
||||
- name: Run coverage
|
||||
run: codecov
|
||||
|
||||
62
.github/workflows/pre-commit-detect-outdated.yml
vendored
Normal file
62
.github/workflows/pre-commit-detect-outdated.yml
vendored
Normal file
@@ -0,0 +1,62 @@
|
||||
# Copyright (c) 2023 Sebastian Pipping <sebastian@pipping.org>
|
||||
# Licensed under the MIT license
|
||||
|
||||
name: Detect outdated pre-commit hooks
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 16 * * 5' # Every Friday 4pm
|
||||
|
||||
# NOTE: This will drop all permissions from GITHUB_TOKEN except metadata read,
|
||||
# and then (re)add the ones listed below:
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
pre_commit_detect_outdated:
|
||||
name: Detect outdated pre-commit hooks
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python 3.12
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: 3.12
|
||||
|
||||
- name: Install pre-commit
|
||||
run: |-
|
||||
pip install \
|
||||
--disable-pip-version-check \
|
||||
--no-warn-script-location \
|
||||
--user \
|
||||
pre-commit
|
||||
echo "PATH=${HOME}/.local/bin:${PATH}" >> "${GITHUB_ENV}"
|
||||
|
||||
- name: Check for outdated hooks
|
||||
run: |-
|
||||
pre-commit autoupdate
|
||||
git diff -- .pre-commit-config.yaml
|
||||
|
||||
- name: Create pull request from changes (if any)
|
||||
id: create-pull-request
|
||||
uses: peter-evans/create-pull-request@v7
|
||||
with:
|
||||
author: 'pre-commit <pre-commit@tools.invalid>'
|
||||
base: master
|
||||
body: |-
|
||||
For your consideration.
|
||||
|
||||
:warning: Please **CLOSE AND RE-OPEN** this pull request so that [further workflow runs get triggered](https://github.com/peter-evans/create-pull-request/blob/main/docs/concepts-guidelines.md#triggering-further-workflow-runs) for this pull request.
|
||||
branch: precommit-autoupdate
|
||||
commit-message: "pre-commit: Autoupdate"
|
||||
delete-branch: true
|
||||
draft: true
|
||||
labels: enhancement
|
||||
title: "pre-commit: Autoupdate"
|
||||
|
||||
- name: Log pull request URL
|
||||
if: "${{ steps.create-pull-request.outputs.pull-request-url }}"
|
||||
run: |
|
||||
echo "Pull request URL is: ${{ steps.create-pull-request.outputs.pull-request-url }}"
|
||||
20
.github/workflows/pre-commit.yml
vendored
Normal file
20
.github/workflows/pre-commit.yml
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
# Copyright (c) 2023 Sebastian Pipping <sebastian@pipping.org>
|
||||
# Licensed under the MIT license
|
||||
|
||||
name: Run pre-commit
|
||||
|
||||
on:
|
||||
- pull_request
|
||||
- push
|
||||
- workflow_dispatch
|
||||
|
||||
jobs:
|
||||
pre-commit:
|
||||
name: Run pre-commit
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: 3.12
|
||||
- uses: pre-commit/action@v3.0.1
|
||||
17
.pre-commit-config.yaml
Normal file
17
.pre-commit-config.yaml
Normal file
@@ -0,0 +1,17 @@
|
||||
# Copyright (c) 2023 Sebastian Pipping <sebastian@pipping.org>
|
||||
# Licensed under the MIT license
|
||||
|
||||
repos:
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.6.6
|
||||
hooks:
|
||||
- id: ruff
|
||||
args: ["--output-format=full"]
|
||||
- id: ruff-format
|
||||
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v4.6.0
|
||||
hooks:
|
||||
- id: check-merge-conflict
|
||||
- id: end-of-file-fixer
|
||||
- id: trailing-whitespace
|
||||
@@ -9,7 +9,7 @@ version: 2
|
||||
build:
|
||||
os: ubuntu-22.04
|
||||
tools:
|
||||
python: "3.11"
|
||||
python: "3.12"
|
||||
|
||||
# Build documentation in the docs/ directory with Sphinx
|
||||
sphinx:
|
||||
@@ -20,3 +20,5 @@ sphinx:
|
||||
python:
|
||||
install:
|
||||
- requirements: docs/requirements.txt
|
||||
- method: pip
|
||||
path: .
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
include README.rst
|
||||
include LICENSE.txt
|
||||
include tox.ini
|
||||
recursive-include tests *
|
||||
recursive-exclude * __pycache__
|
||||
recursive-exclude * *.py[co]
|
||||
|
||||
@@ -4,7 +4,7 @@ VCR.py 📼
|
||||
###########
|
||||
|
||||
|
||||
|PyPI| |Python versions| |Build Status| |CodeCov| |Gitter| |CodeStyleBlack|
|
||||
|PyPI| |Python versions| |Build Status| |CodeCov| |Gitter|
|
||||
|
||||
----
|
||||
|
||||
@@ -70,6 +70,3 @@ more details
|
||||
.. |CodeCov| image:: https://codecov.io/gh/kevin1024/vcrpy/branch/master/graph/badge.svg
|
||||
:target: https://codecov.io/gh/kevin1024/vcrpy
|
||||
:alt: Code Coverage Status
|
||||
.. |CodeStyleBlack| image:: https://img.shields.io/badge/code%20style-black-000000.svg
|
||||
:target: https://github.com/psf/black
|
||||
:alt: Code Style: black
|
||||
|
||||
@@ -7,6 +7,21 @@ For a full list of triaged issues, bugs and PRs and what release they are target
|
||||
|
||||
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.
|
||||
|
||||
- 6.0.2
|
||||
- Ensure body is consumed only once (#846) - thanks @sathieu
|
||||
- Permit urllib3 2.x for non-PyPy Python >=3.10
|
||||
- Fix typos in test commands - thanks @chuckwondo
|
||||
- Several test and workflow improvements - thanks @hartwork and @graingert
|
||||
- 6.0.1
|
||||
- Bugfix with to Tornado cassette generator (thanks @graingert)
|
||||
- 6.0.0
|
||||
- BREAKING: Fix issue with httpx support (thanks @parkerhancock) in #784. NOTE: You may have to recreate some of your cassettes produced in previous releases due to the binary format being saved incorrectly in previous releases
|
||||
- BREAKING: Drop support for `boto` (vcrpy still supports boto3, but is dropping the deprecated `boto` support in this release. (thanks @jairhenrique)
|
||||
- Fix compatibility issue with Python 3.12 (thanks @hartwork)
|
||||
- Drop simplejson (fixes some compatibility issues) (thanks @jairhenrique)
|
||||
- Run CI on Python 3.12 and PyPy 3.9-3.10 (thanks @mgorny)
|
||||
- Various linting and docs improvements (thanks @jairhenrique)
|
||||
- Tornado fixes (thanks @graingert)
|
||||
- 5.1.0
|
||||
- Use ruff for linting (instead of current flake8/isort/pyflakes) - thanks @jairhenrique
|
||||
- Enable rule B (flake8-bugbear) on ruff - thanks @jairhenrique
|
||||
@@ -23,6 +38,7 @@ All help in providing PRs to close out bug issues is appreciated. Even if that i
|
||||
- 5.0.0
|
||||
- BREAKING CHANGE: Drop support for Python 3.7. 3.7 is EOL as of 6/27/23 Thanks @jairhenrique
|
||||
- BREAKING CHANGE: Custom Cassette persisters no longer catch ValueError. If you have implemented a custom persister (has anyone implemented a custom persister? Let us know!) then you will need to throw a CassetteNotFoundError when unable to find a cassette. See #681 for discussion and reason for this change. Thanks @amosjyng for the PR and the review from @hartwork
|
||||
|
||||
- 4.4.0
|
||||
- HUGE thanks to @hartwork for all the work done on this release!
|
||||
- Bring vcr/unittest in to vcrpy as a full feature of vcr instead of a separate library. Big thanks to @hartwork for doing this and to @agriffis for originally creating the library
|
||||
@@ -286,4 +302,3 @@ All help in providing PRs to close out bug issues is appreciated. Even if that i
|
||||
- Add support for requests / urllib3
|
||||
- 0.0.1
|
||||
- Initial Release
|
||||
|
||||
|
||||
@@ -83,39 +83,21 @@ The PR reviewer is a second set of eyes to see if:
|
||||
Running VCR's test suite
|
||||
------------------------
|
||||
|
||||
The tests are all run automatically on `Travis
|
||||
CI <https://travis-ci.org/kevin1024/vcrpy>`__, but you can also run them
|
||||
yourself using `pytest <http://pytest.org/>`__ and
|
||||
`Tox <http://tox.testrun.org/>`__.
|
||||
The tests are all run automatically on `Github Actions CI <https://github.com/kevin1024/vcrpy/actions>`__,
|
||||
but you can also run them yourself using `pytest <http://pytest.org/>`__.
|
||||
|
||||
Tox will automatically run them in all environments VCR.py supports if they are available on your `PATH`. Alternatively you can use `tox-pyenv <https://pypi.org/project/tox-pyenv/>`_ with
|
||||
`pyenv <https://github.com/pyenv/pyenv>`_.
|
||||
We recommend you read the documentation for each and see the section further below.
|
||||
|
||||
The test suite is pretty big and slow, but you can tell tox to only run specific tests like this::
|
||||
|
||||
tox -e {pyNN}-{HTTP_LIBRARY} -- <pytest flags passed through>
|
||||
|
||||
tox -e py38-requests -- -v -k "'test_status_code or test_gzip'"
|
||||
tox -e py38-requests -- -v --last-failed
|
||||
|
||||
This will run only tests that look like ``test_status_code`` or
|
||||
``test_gzip`` in the test suite, and only in the python 3.8 environment
|
||||
that has ``requests`` installed.
|
||||
|
||||
Also, in order for the boto tests to run, you will need an AWS key.
|
||||
Refer to the `boto
|
||||
documentation <https://boto.readthedocs.io/en/latest/getting_started.html>`__
|
||||
for how to set this up. I have marked the boto tests as optional in
|
||||
In order for the boto3 tests to run, you will need an AWS key.
|
||||
Refer to the `boto3
|
||||
documentation <https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/index.html>`__
|
||||
for how to set this up. I have marked the boto3 tests as optional in
|
||||
Travis so you don't have to worry about them failing if you submit a
|
||||
pull request.
|
||||
|
||||
Using PyEnv with VCR's test suite
|
||||
Using Pyenv with VCR's test suite
|
||||
---------------------------------
|
||||
|
||||
PyEnv is a tool for managing multiple installation of python on your system.
|
||||
Pyenv is a tool for managing multiple installation of python on your system.
|
||||
See the full documentation at their `github <https://github.com/pyenv/pyenv>`_
|
||||
but we are also going to use `tox-pyenv <https://pypi.org/project/tox-pyenv/>`_
|
||||
in this example::
|
||||
|
||||
git clone https://github.com/pyenv/pyenv ~/.pyenv
|
||||
@@ -126,27 +108,21 @@ in this example::
|
||||
# Setup shim paths
|
||||
eval "$(pyenv init -)"
|
||||
|
||||
# Setup your local system tox tooling
|
||||
pip3 install tox tox-pyenv
|
||||
|
||||
# Install supported versions (at time of writing), this does not activate them
|
||||
pyenv install 3.8.0 pypy3.8
|
||||
pyenv install 3.12.0 pypy3.10
|
||||
|
||||
# This activates them
|
||||
pyenv local 3.8.0 pypy3.8
|
||||
pyenv local 3.12.0 pypy3.10
|
||||
|
||||
# Run the whole test suite
|
||||
tox
|
||||
|
||||
# Run the whole test suite or just part of it
|
||||
tox -e lint
|
||||
tox -e py38-requests
|
||||
pip install .[tests]
|
||||
./runtests.sh
|
||||
|
||||
|
||||
Troubleshooting on MacOSX
|
||||
-------------------------
|
||||
|
||||
If you have this kind of error when running tox :
|
||||
If you have this kind of error when running tests :
|
||||
|
||||
.. code:: python
|
||||
|
||||
|
||||
@@ -14,7 +14,6 @@ VCR.py supports Python 3.8+, and `pypy <http://pypy.org>`__.
|
||||
The following HTTP libraries are supported:
|
||||
|
||||
- ``aiohttp``
|
||||
- ``boto``
|
||||
- ``boto3``
|
||||
- ``http.client``
|
||||
- ``httplib2``
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
sphinx<8
|
||||
sphinx_rtd_theme==1.2.2
|
||||
sphinx_rtd_theme==2.0.0
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
[tool.black]
|
||||
line-length=110
|
||||
|
||||
[tool.codespell]
|
||||
skip = '.git,*.pdf,*.svg,.tox'
|
||||
ignore-regex = "\\\\[fnrstv]"
|
||||
@@ -8,8 +5,14 @@ ignore-regex = "\\\\[fnrstv]"
|
||||
# ignore-words-list = ''
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
markers = [
|
||||
"online",
|
||||
addopts = [
|
||||
"--strict-config",
|
||||
"--strict-markers",
|
||||
]
|
||||
markers = ["online"]
|
||||
filterwarnings = [
|
||||
"error",
|
||||
'''ignore:datetime\.datetime\.utcfromtimestamp\(\) is deprecated and scheduled for removal in a future version.*:DeprecationWarning''',
|
||||
]
|
||||
|
||||
[tool.ruff]
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
#!/bin/bash
|
||||
|
||||
# https://blog.ionelmc.ro/2015/04/14/tox-tricks-and-patterns/#when-it-inevitably-leads-to-shell-scripts
|
||||
# If you are getting an INVOCATION ERROR for this script then there is
|
||||
# a good chance you are running on Windows.
|
||||
# You can and should use WSL for running tox on Windows when it calls bash scripts.
|
||||
# If you are getting an INVOCATION ERROR for this script then there is a good chance you are running on Windows.
|
||||
# You can and should use WSL for running tests on Windows when it calls bash scripts.
|
||||
REQUESTS_CA_BUNDLE=`python3 -m pytest_httpbin.certs` exec pytest "$@"
|
||||
|
||||
42
setup.py
42
setup.py
@@ -3,10 +3,8 @@
|
||||
import codecs
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
|
||||
from setuptools import find_packages, setup
|
||||
from setuptools.command.test import test as TestCommand
|
||||
|
||||
long_description = open("README.rst").read()
|
||||
here = os.path.abspath(os.path.dirname(__file__))
|
||||
@@ -28,43 +26,38 @@ def find_version(*file_paths):
|
||||
raise RuntimeError("Unable to find version string.")
|
||||
|
||||
|
||||
class PyTest(TestCommand):
|
||||
def finalize_options(self):
|
||||
TestCommand.finalize_options(self)
|
||||
self.test_args = []
|
||||
self.test_suite = True
|
||||
|
||||
def run_tests(self):
|
||||
# import here, cause outside the eggs aren't loaded
|
||||
import pytest
|
||||
|
||||
errno = pytest.main(self.test_args)
|
||||
sys.exit(errno)
|
||||
|
||||
|
||||
install_requires = [
|
||||
"PyYAML",
|
||||
"wrapt",
|
||||
"yarl",
|
||||
# Support for urllib3 >=2 needs Python >=3.10
|
||||
# so we need to block urllib3 >=2 for Python <3.10 for now.
|
||||
# Support for urllib3 >=2 needs CPython >=3.10
|
||||
# so we need to block urllib3 >=2 for Python <3.10 and PyPy for now.
|
||||
# Note that vcrpy would work fine without any urllib3 around,
|
||||
# so this block and the dependency can be dropped at some point
|
||||
# in the future. For more Details:
|
||||
# https://github.com/kevin1024/vcrpy/pull/699#issuecomment-1551439663
|
||||
"urllib3 <2; python_version <'3.10'",
|
||||
# https://github.com/kevin1024/vcrpy/pull/775#issuecomment-1847849962
|
||||
"urllib3 <2; platform_python_implementation =='PyPy'",
|
||||
# Workaround for Poetry with CPython >= 3.10, problem description at:
|
||||
# https://github.com/kevin1024/vcrpy/pull/826
|
||||
"urllib3; platform_python_implementation !='PyPy' and python_version >='3.10'",
|
||||
]
|
||||
|
||||
tests_require = [
|
||||
extras_require = {
|
||||
"tests": [
|
||||
"aiohttp",
|
||||
"boto3",
|
||||
"httplib2",
|
||||
"httpx",
|
||||
"pytest",
|
||||
"pytest-aiohttp",
|
||||
"pytest-asyncio",
|
||||
"pytest-cov",
|
||||
"pytest-httpbin",
|
||||
"requests>=2.16.2",
|
||||
"pytest",
|
||||
"requests>=2.22.0",
|
||||
"tornado",
|
||||
"urllib3",
|
||||
# Needed to un-break httpbin 0.7.0. For httpbin >=0.7.1 and after,
|
||||
# this pin and the dependency itself can be removed, provided
|
||||
# that the related bug in httpbin has been fixed:
|
||||
@@ -72,7 +65,8 @@ tests_require = [
|
||||
# https://github.com/postmanlabs/httpbin/issues/673
|
||||
# https://github.com/postmanlabs/httpbin/pull/674
|
||||
"Werkzeug==2.0.3",
|
||||
]
|
||||
],
|
||||
}
|
||||
|
||||
setup(
|
||||
name="vcrpy",
|
||||
@@ -87,7 +81,8 @@ setup(
|
||||
python_requires=">=3.8",
|
||||
install_requires=install_requires,
|
||||
license="MIT",
|
||||
tests_require=tests_require,
|
||||
extras_require=extras_require,
|
||||
tests_require=extras_require["tests"],
|
||||
classifiers=[
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
"Environment :: Console",
|
||||
@@ -98,6 +93,7 @@ setup(
|
||||
"Programming Language :: Python :: 3.9",
|
||||
"Programming Language :: Python :: 3.10",
|
||||
"Programming Language :: Python :: 3.11",
|
||||
"Programming Language :: Python :: 3.12",
|
||||
"Programming Language :: Python :: 3 :: Only",
|
||||
"Programming Language :: Python :: Implementation :: CPython",
|
||||
"Programming Language :: Python :: Implementation :: PyPy",
|
||||
|
||||
0
tests/__init__.py
Normal file
0
tests/__init__.py
Normal file
@@ -5,7 +5,7 @@ import aiohttp
|
||||
|
||||
|
||||
async def aiohttp_request(loop, method, url, output="text", encoding="utf-8", content_type=None, **kwargs):
|
||||
session = aiohttp.ClientSession(loop=loop)
|
||||
async with aiohttp.ClientSession(loop=loop) as session:
|
||||
response_ctx = session.request(method, url, **kwargs)
|
||||
|
||||
response = await response_ctx.__aenter__()
|
||||
|
||||
41
tests/integration/cassettes/gzip_httpx_old_format.yaml
Normal file
41
tests/integration/cassettes/gzip_httpx_old_format.yaml
Normal file
@@ -0,0 +1,41 @@
|
||||
interactions:
|
||||
- request:
|
||||
body: ''
|
||||
headers:
|
||||
accept:
|
||||
- '*/*'
|
||||
accept-encoding:
|
||||
- gzip, deflate, br
|
||||
connection:
|
||||
- keep-alive
|
||||
host:
|
||||
- httpbin.org
|
||||
user-agent:
|
||||
- python-httpx/0.23.0
|
||||
method: GET
|
||||
uri: https://httpbin.org/gzip
|
||||
response:
|
||||
content: "{\n \"gzipped\": true, \n \"headers\": {\n \"Accept\": \"*/*\",
|
||||
\n \"Accept-Encoding\": \"gzip, deflate, br\", \n \"Host\": \"httpbin.org\",
|
||||
\n \"User-Agent\": \"python-httpx/0.23.0\", \n \"X-Amzn-Trace-Id\": \"Root=1-62a62a8d-5f39b5c50c744da821d6ea99\"\n
|
||||
\ }, \n \"method\": \"GET\", \n \"origin\": \"146.200.25.115\"\n}\n"
|
||||
headers:
|
||||
Access-Control-Allow-Credentials:
|
||||
- 'true'
|
||||
Access-Control-Allow-Origin:
|
||||
- '*'
|
||||
Connection:
|
||||
- keep-alive
|
||||
Content-Encoding:
|
||||
- gzip
|
||||
Content-Length:
|
||||
- '230'
|
||||
Content-Type:
|
||||
- application/json
|
||||
Date:
|
||||
- Sun, 12 Jun 2022 18:03:57 GMT
|
||||
Server:
|
||||
- gunicorn/19.9.0
|
||||
http_version: HTTP/1.1
|
||||
status_code: 200
|
||||
version: 1
|
||||
42
tests/integration/cassettes/gzip_requests.yaml
Normal file
42
tests/integration/cassettes/gzip_requests.yaml
Normal file
@@ -0,0 +1,42 @@
|
||||
interactions:
|
||||
- request:
|
||||
body: null
|
||||
headers:
|
||||
Accept:
|
||||
- '*/*'
|
||||
Accept-Encoding:
|
||||
- gzip, deflate, br
|
||||
Connection:
|
||||
- keep-alive
|
||||
User-Agent:
|
||||
- python-requests/2.28.0
|
||||
method: GET
|
||||
uri: https://httpbin.org/gzip
|
||||
response:
|
||||
body:
|
||||
string: !!binary |
|
||||
H4sIAKwrpmIA/z2OSwrCMBCG956izLIkfQSxkl2RogfQA9R2bIM1iUkqaOndnYDIrGa+/zELDB9l
|
||||
LfYgg5uRwYhtj86DXKDuOrQBJKR5Cuy38kZ3pld6oHu0sqTH29QGZMnVkepgtMYuKKNJcEe0vJ3U
|
||||
C4mcjI9hpaiygqaUW7ETFYGLR8frAXXE9h1Go7nD54w++FxkYp8VsDJ4IBH6E47NmVzGqUHFkn8g
|
||||
rJsvp2omYs8AAAA=
|
||||
headers:
|
||||
Access-Control-Allow-Credentials:
|
||||
- 'true'
|
||||
Access-Control-Allow-Origin:
|
||||
- '*'
|
||||
Connection:
|
||||
- Close
|
||||
Content-Encoding:
|
||||
- gzip
|
||||
Content-Length:
|
||||
- '182'
|
||||
Content-Type:
|
||||
- application/json
|
||||
Date:
|
||||
- Sun, 12 Jun 2022 18:08:44 GMT
|
||||
Server:
|
||||
- Pytest-HTTPBIN/0.1.0
|
||||
status:
|
||||
code: 200
|
||||
message: great
|
||||
version: 1
|
||||
@@ -1,32 +0,0 @@
|
||||
import os
|
||||
import ssl
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.fixture(params=["https", "http"])
|
||||
def scheme(request):
|
||||
"""Fixture that returns both http and https."""
|
||||
return request.param
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mockbin(scheme):
|
||||
return scheme + "://mockbin.org"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mockbin_request_url(mockbin):
|
||||
return mockbin + "/request"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def httpbin_ssl_context():
|
||||
ssl_ca_location = os.environ["REQUESTS_CA_BUNDLE"]
|
||||
ssl_cert_location = os.environ["REQUESTS_CA_BUNDLE"].replace("cacert.pem", "cert.pem")
|
||||
ssl_key_location = os.environ["REQUESTS_CA_BUNDLE"].replace("cacert.pem", "key.pem")
|
||||
|
||||
ssl_context = ssl.create_default_context(cafile=ssl_ca_location)
|
||||
ssl_context.load_cert_chain(ssl_cert_location, ssl_key_location)
|
||||
|
||||
return ssl_context
|
||||
@@ -1,8 +1,9 @@
|
||||
import contextlib
|
||||
import logging
|
||||
import ssl
|
||||
import urllib.parse
|
||||
|
||||
import pytest
|
||||
import pytest_httpbin.certs
|
||||
|
||||
import vcr
|
||||
|
||||
@@ -12,12 +13,14 @@ aiohttp = pytest.importorskip("aiohttp")
|
||||
|
||||
from .aiohttp_utils import aiohttp_app, aiohttp_request # noqa: E402
|
||||
|
||||
HTTPBIN_SSL_CONTEXT = ssl.create_default_context(cafile=pytest_httpbin.certs.where())
|
||||
|
||||
|
||||
def run_in_loop(fn):
|
||||
with contextlib.closing(asyncio.new_event_loop()) as loop:
|
||||
asyncio.set_event_loop(loop)
|
||||
task = loop.create_task(fn(loop))
|
||||
return loop.run_until_complete(task)
|
||||
async def wrapper():
|
||||
return await fn(asyncio.get_running_loop())
|
||||
|
||||
return asyncio.run(wrapper())
|
||||
|
||||
|
||||
def request(method, url, output="text", **kwargs):
|
||||
@@ -36,8 +39,8 @@ def post(url, output="text", **kwargs):
|
||||
|
||||
|
||||
@pytest.mark.online
|
||||
def test_status(tmpdir, mockbin_request_url):
|
||||
url = mockbin_request_url
|
||||
def test_status(tmpdir, httpbin):
|
||||
url = httpbin.url
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join("status.yaml"))):
|
||||
response, _ = get(url)
|
||||
@@ -50,8 +53,8 @@ def test_status(tmpdir, mockbin_request_url):
|
||||
|
||||
@pytest.mark.online
|
||||
@pytest.mark.parametrize("auth", [None, aiohttp.BasicAuth("vcrpy", "test")])
|
||||
def test_headers(tmpdir, auth, mockbin_request_url):
|
||||
url = mockbin_request_url
|
||||
def test_headers(tmpdir, auth, httpbin):
|
||||
url = httpbin.url
|
||||
with vcr.use_cassette(str(tmpdir.join("headers.yaml"))):
|
||||
response, _ = get(url, auth=auth)
|
||||
|
||||
@@ -67,8 +70,8 @@ def test_headers(tmpdir, auth, mockbin_request_url):
|
||||
|
||||
|
||||
@pytest.mark.online
|
||||
def test_case_insensitive_headers(tmpdir, mockbin_request_url):
|
||||
url = mockbin_request_url
|
||||
def test_case_insensitive_headers(tmpdir, httpbin):
|
||||
url = httpbin.url
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join("whatever.yaml"))):
|
||||
_, _ = get(url)
|
||||
@@ -81,8 +84,8 @@ def test_case_insensitive_headers(tmpdir, mockbin_request_url):
|
||||
|
||||
|
||||
@pytest.mark.online
|
||||
def test_text(tmpdir, mockbin_request_url):
|
||||
url = mockbin_request_url
|
||||
def test_text(tmpdir, httpbin):
|
||||
url = httpbin.url
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join("text.yaml"))):
|
||||
_, response_text = get(url)
|
||||
@@ -94,8 +97,8 @@ def test_text(tmpdir, mockbin_request_url):
|
||||
|
||||
|
||||
@pytest.mark.online
|
||||
def test_json(tmpdir, mockbin_request_url):
|
||||
url = mockbin_request_url
|
||||
def test_json(tmpdir, httpbin):
|
||||
url = httpbin.url + "/json"
|
||||
headers = {"Content-Type": "application/json"}
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join("json.yaml"))):
|
||||
@@ -108,8 +111,8 @@ def test_json(tmpdir, mockbin_request_url):
|
||||
|
||||
|
||||
@pytest.mark.online
|
||||
def test_binary(tmpdir, mockbin_request_url):
|
||||
url = mockbin_request_url + "/image/png"
|
||||
def test_binary(tmpdir, httpbin):
|
||||
url = httpbin.url + "/image/png"
|
||||
with vcr.use_cassette(str(tmpdir.join("binary.yaml"))):
|
||||
_, response_binary = get(url, output="raw")
|
||||
|
||||
@@ -120,8 +123,8 @@ def test_binary(tmpdir, mockbin_request_url):
|
||||
|
||||
|
||||
@pytest.mark.online
|
||||
def test_stream(tmpdir, mockbin_request_url):
|
||||
url = mockbin_request_url
|
||||
def test_stream(tmpdir, httpbin):
|
||||
url = httpbin.url
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join("stream.yaml"))):
|
||||
_, body = get(url, output="raw") # Do not use stream here, as the stream is exhausted by vcr
|
||||
@@ -134,10 +137,10 @@ def test_stream(tmpdir, mockbin_request_url):
|
||||
|
||||
@pytest.mark.online
|
||||
@pytest.mark.parametrize("body", ["data", "json"])
|
||||
def test_post(tmpdir, body, caplog, mockbin_request_url):
|
||||
def test_post(tmpdir, body, caplog, httpbin):
|
||||
caplog.set_level(logging.INFO)
|
||||
data = {"key1": "value1", "key2": "value2"}
|
||||
url = mockbin_request_url
|
||||
url = httpbin.url
|
||||
with vcr.use_cassette(str(tmpdir.join("post.yaml"))):
|
||||
_, response_json = post(url, **{body: data})
|
||||
|
||||
@@ -159,14 +162,14 @@ def test_post(tmpdir, body, caplog, mockbin_request_url):
|
||||
|
||||
|
||||
@pytest.mark.online
|
||||
def test_params(tmpdir, mockbin_request_url):
|
||||
url = mockbin_request_url + "?d=d"
|
||||
def test_params(tmpdir, httpbin):
|
||||
url = httpbin.url + "/get?d=d"
|
||||
headers = {"Content-Type": "application/json"}
|
||||
params = {"a": 1, "b": 2, "c": "c"}
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join("get.yaml"))) as cassette:
|
||||
_, response_json = get(url, output="json", params=params, headers=headers)
|
||||
assert response_json["queryString"] == {"a": "1", "b": "2", "c": "c", "d": "d"}
|
||||
assert response_json["args"] == {"a": "1", "b": "2", "c": "c", "d": "d"}
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join("get.yaml"))) as cassette:
|
||||
_, cassette_response_json = get(url, output="json", params=params, headers=headers)
|
||||
@@ -175,8 +178,8 @@ def test_params(tmpdir, mockbin_request_url):
|
||||
|
||||
|
||||
@pytest.mark.online
|
||||
def test_params_same_url_distinct_params(tmpdir, mockbin_request_url):
|
||||
url = mockbin_request_url
|
||||
def test_params_same_url_distinct_params(tmpdir, httpbin):
|
||||
url = httpbin.url + "/json"
|
||||
headers = {"Content-Type": "application/json"}
|
||||
params = {"a": 1, "b": 2, "c": "c"}
|
||||
|
||||
@@ -195,8 +198,8 @@ def test_params_same_url_distinct_params(tmpdir, mockbin_request_url):
|
||||
|
||||
|
||||
@pytest.mark.online
|
||||
def test_params_on_url(tmpdir, mockbin_request_url):
|
||||
url = mockbin_request_url + "?a=1&b=foo"
|
||||
def test_params_on_url(tmpdir, httpbin):
|
||||
url = httpbin.url + "/get?a=1&b=foo"
|
||||
headers = {"Content-Type": "application/json"}
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join("get.yaml"))) as cassette:
|
||||
@@ -260,9 +263,15 @@ def test_aiohttp_test_client_json(aiohttp_client, tmpdir):
|
||||
assert cassette.play_count == 1
|
||||
|
||||
|
||||
def test_cleanup_from_pytest_asyncio():
|
||||
# work around https://github.com/pytest-dev/pytest-asyncio/issues/724
|
||||
asyncio.get_event_loop().close()
|
||||
asyncio.set_event_loop(None)
|
||||
|
||||
|
||||
@pytest.mark.online
|
||||
def test_redirect(tmpdir, mockbin):
|
||||
url = mockbin + "/redirect/302/2"
|
||||
def test_redirect(tmpdir, httpbin):
|
||||
url = httpbin.url + "/redirect/2"
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join("redirect.yaml"))):
|
||||
response, _ = get(url)
|
||||
@@ -284,9 +293,9 @@ def test_redirect(tmpdir, mockbin):
|
||||
|
||||
|
||||
@pytest.mark.online
|
||||
def test_not_modified(tmpdir, mockbin):
|
||||
def test_not_modified(tmpdir, httpbin):
|
||||
"""It doesn't try to redirect on 304"""
|
||||
url = mockbin + "/status/304"
|
||||
url = httpbin.url + "/status/304"
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join("not_modified.yaml"))):
|
||||
response, _ = get(url)
|
||||
@@ -302,13 +311,13 @@ def test_not_modified(tmpdir, mockbin):
|
||||
|
||||
|
||||
@pytest.mark.online
|
||||
def test_double_requests(tmpdir, mockbin_request_url):
|
||||
def test_double_requests(tmpdir, httpbin):
|
||||
"""We should capture, record, and replay all requests and response chains,
|
||||
even if there are duplicate ones.
|
||||
|
||||
We should replay in the order we saw them.
|
||||
"""
|
||||
url = mockbin_request_url
|
||||
url = httpbin.url
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join("text.yaml"))):
|
||||
_, response_text1 = get(url, output="text")
|
||||
@@ -333,7 +342,7 @@ def test_double_requests(tmpdir, mockbin_request_url):
|
||||
assert cassette.play_count == 2
|
||||
|
||||
|
||||
def test_cookies(httpbin_both, httpbin_ssl_context, tmpdir):
|
||||
def test_cookies(httpbin_both, tmpdir):
|
||||
async def run(loop):
|
||||
cookies_url = httpbin_both.url + (
|
||||
"/response-headers?"
|
||||
@@ -348,12 +357,12 @@ def test_cookies(httpbin_both, httpbin_ssl_context, tmpdir):
|
||||
# ------------------------- Record -------------------------- #
|
||||
with vcr.use_cassette(tmp) as cassette:
|
||||
async with aiohttp.ClientSession(loop=loop, cookie_jar=aiohttp.CookieJar(unsafe=True)) as session:
|
||||
cookies_resp = await session.get(cookies_url, ssl=httpbin_ssl_context)
|
||||
cookies_resp = await session.get(cookies_url, ssl=HTTPBIN_SSL_CONTEXT)
|
||||
home_resp = await session.get(
|
||||
home_url,
|
||||
cookies=req_cookies,
|
||||
headers=req_headers,
|
||||
ssl=httpbin_ssl_context,
|
||||
ssl=HTTPBIN_SSL_CONTEXT,
|
||||
)
|
||||
assert cassette.play_count == 0
|
||||
assert_responses(cookies_resp, home_resp)
|
||||
@@ -361,12 +370,12 @@ def test_cookies(httpbin_both, httpbin_ssl_context, tmpdir):
|
||||
# -------------------------- Play --------------------------- #
|
||||
with vcr.use_cassette(tmp, record_mode=vcr.mode.NONE) as cassette:
|
||||
async with aiohttp.ClientSession(loop=loop, cookie_jar=aiohttp.CookieJar(unsafe=True)) as session:
|
||||
cookies_resp = await session.get(cookies_url, ssl=httpbin_ssl_context)
|
||||
cookies_resp = await session.get(cookies_url, ssl=HTTPBIN_SSL_CONTEXT)
|
||||
home_resp = await session.get(
|
||||
home_url,
|
||||
cookies=req_cookies,
|
||||
headers=req_headers,
|
||||
ssl=httpbin_ssl_context,
|
||||
ssl=HTTPBIN_SSL_CONTEXT,
|
||||
)
|
||||
assert cassette.play_count == 2
|
||||
assert_responses(cookies_resp, home_resp)
|
||||
@@ -383,7 +392,7 @@ def test_cookies(httpbin_both, httpbin_ssl_context, tmpdir):
|
||||
run_in_loop(run)
|
||||
|
||||
|
||||
def test_cookies_redirect(httpbin_both, httpbin_ssl_context, tmpdir):
|
||||
def test_cookies_redirect(httpbin_both, tmpdir):
|
||||
async def run(loop):
|
||||
# Sets cookie as provided by the query string and redirects
|
||||
cookies_url = httpbin_both.url + "/cookies/set?Cookie_1=Val_1"
|
||||
@@ -392,7 +401,7 @@ def test_cookies_redirect(httpbin_both, httpbin_ssl_context, tmpdir):
|
||||
# ------------------------- Record -------------------------- #
|
||||
with vcr.use_cassette(tmp) as cassette:
|
||||
async with aiohttp.ClientSession(loop=loop, cookie_jar=aiohttp.CookieJar(unsafe=True)) as session:
|
||||
cookies_resp = await session.get(cookies_url, ssl=httpbin_ssl_context)
|
||||
cookies_resp = await session.get(cookies_url, ssl=HTTPBIN_SSL_CONTEXT)
|
||||
assert not cookies_resp.cookies
|
||||
cookies = session.cookie_jar.filter_cookies(cookies_url)
|
||||
assert cookies["Cookie_1"].value == "Val_1"
|
||||
@@ -403,7 +412,7 @@ def test_cookies_redirect(httpbin_both, httpbin_ssl_context, tmpdir):
|
||||
# -------------------------- Play --------------------------- #
|
||||
with vcr.use_cassette(tmp, record_mode=vcr.mode.NONE) as cassette:
|
||||
async with aiohttp.ClientSession(loop=loop, cookie_jar=aiohttp.CookieJar(unsafe=True)) as session:
|
||||
cookies_resp = await session.get(cookies_url, ssl=httpbin_ssl_context)
|
||||
cookies_resp = await session.get(cookies_url, ssl=HTTPBIN_SSL_CONTEXT)
|
||||
assert not cookies_resp.cookies
|
||||
cookies = session.cookie_jar.filter_cookies(cookies_url)
|
||||
assert cookies["Cookie_1"].value == "Val_1"
|
||||
@@ -417,7 +426,7 @@ def test_cookies_redirect(httpbin_both, httpbin_ssl_context, tmpdir):
|
||||
"Cookie_1=Val_1; Expires=Wed, 21 Oct 2015 07:28:00 GMT",
|
||||
]
|
||||
async with aiohttp.ClientSession(loop=loop, cookie_jar=aiohttp.CookieJar(unsafe=True)) as session:
|
||||
cookies_resp = await session.get(cookies_url, ssl=httpbin_ssl_context)
|
||||
cookies_resp = await session.get(cookies_url, ssl=HTTPBIN_SSL_CONTEXT)
|
||||
assert not cookies_resp.cookies
|
||||
cookies = session.cookie_jar.filter_cookies(cookies_url)
|
||||
assert cookies["Cookie_1"].value == "Val_1"
|
||||
@@ -426,18 +435,18 @@ def test_cookies_redirect(httpbin_both, httpbin_ssl_context, tmpdir):
|
||||
|
||||
|
||||
@pytest.mark.online
|
||||
def test_not_allow_redirects(tmpdir, mockbin):
|
||||
url = mockbin + "/redirect/308/5"
|
||||
def test_not_allow_redirects(tmpdir, httpbin):
|
||||
url = httpbin + "/redirect-to?url=.%2F&status_code=308"
|
||||
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.url.path == "/redirect-to"
|
||||
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.url.path == "/redirect-to"
|
||||
assert response.status == 308
|
||||
assert cassette.play_count == 1
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ def test_basic_json_use(tmpdir, httpbin):
|
||||
test_fixture = str(tmpdir.join("synopsis.json"))
|
||||
with vcr.use_cassette(test_fixture, serializer="json"):
|
||||
response = urlopen(httpbin.url).read()
|
||||
assert b"difficult sometimes" in response
|
||||
assert b"HTTP Request & Response Service" in response
|
||||
|
||||
|
||||
def test_patched_content(tmpdir, httpbin):
|
||||
|
||||
@@ -1,82 +0,0 @@
|
||||
from configparser import DuplicateSectionError
|
||||
|
||||
import pytest
|
||||
|
||||
import vcr
|
||||
|
||||
boto = pytest.importorskip("boto")
|
||||
|
||||
import boto # noqa
|
||||
import boto.iam # noqa
|
||||
from boto.s3.connection import S3Connection # noqa
|
||||
from boto.s3.key import Key # noqa
|
||||
|
||||
|
||||
def test_boto_stubs(tmpdir):
|
||||
with vcr.use_cassette(str(tmpdir.join("boto-stubs.yml"))):
|
||||
# Perform the imports within the patched context so that
|
||||
# CertValidatingHTTPSConnection refers to the patched version.
|
||||
from boto.https_connection import CertValidatingHTTPSConnection
|
||||
|
||||
from vcr.stubs.boto_stubs import VCRCertValidatingHTTPSConnection
|
||||
|
||||
# Prove that the class was patched by the stub and that we can instantiate it.
|
||||
assert issubclass(CertValidatingHTTPSConnection, VCRCertValidatingHTTPSConnection)
|
||||
CertValidatingHTTPSConnection("hostname.does.not.matter")
|
||||
|
||||
|
||||
def test_boto_without_vcr():
|
||||
s3_conn = S3Connection()
|
||||
s3_bucket = s3_conn.get_bucket("boto-demo-1394171994") # a bucket you can access
|
||||
k = Key(s3_bucket)
|
||||
k.key = "test.txt"
|
||||
k.set_contents_from_string("hello world i am a string")
|
||||
|
||||
|
||||
def test_boto_medium_difficulty(tmpdir):
|
||||
s3_conn = S3Connection()
|
||||
s3_bucket = s3_conn.get_bucket("boto-demo-1394171994") # a bucket you can access
|
||||
with vcr.use_cassette(str(tmpdir.join("boto-medium.yml"))):
|
||||
k = Key(s3_bucket)
|
||||
k.key = "test.txt"
|
||||
k.set_contents_from_string("hello world i am a string")
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join("boto-medium.yml"))):
|
||||
k = Key(s3_bucket)
|
||||
k.key = "test.txt"
|
||||
k.set_contents_from_string("hello world i am a string")
|
||||
|
||||
|
||||
def test_boto_hardcore_mode(tmpdir):
|
||||
with vcr.use_cassette(str(tmpdir.join("boto-hardcore.yml"))):
|
||||
s3_conn = S3Connection()
|
||||
s3_bucket = s3_conn.get_bucket("boto-demo-1394171994") # a bucket you can access
|
||||
k = Key(s3_bucket)
|
||||
k.key = "test.txt"
|
||||
k.set_contents_from_string("hello world i am a string")
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join("boto-hardcore.yml"))):
|
||||
s3_conn = S3Connection()
|
||||
s3_bucket = s3_conn.get_bucket("boto-demo-1394171994") # a bucket you can access
|
||||
k = Key(s3_bucket)
|
||||
k.key = "test.txt"
|
||||
k.set_contents_from_string("hello world i am a string")
|
||||
|
||||
|
||||
def test_boto_iam(tmpdir):
|
||||
try:
|
||||
boto.config.add_section("Boto")
|
||||
except DuplicateSectionError:
|
||||
pass
|
||||
# Ensure that boto uses HTTPS
|
||||
boto.config.set("Boto", "is_secure", "true")
|
||||
# Ensure that boto uses CertValidatingHTTPSConnection
|
||||
boto.config.set("Boto", "https_validate_certificates", "true")
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join("boto-iam.yml"))):
|
||||
iam_conn = boto.iam.connect_to_region("universal")
|
||||
iam_conn.get_all_users()
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join("boto-iam.yml"))):
|
||||
iam_conn = boto.iam.connect_to_region("universal")
|
||||
iam_conn.get_all_users()
|
||||
@@ -8,12 +8,12 @@ import vcr
|
||||
|
||||
|
||||
@pytest.mark.online
|
||||
def test_set_serializer_default_config(tmpdir, mockbin_request_url):
|
||||
def test_set_serializer_default_config(tmpdir, httpbin):
|
||||
my_vcr = vcr.VCR(serializer="json")
|
||||
|
||||
with my_vcr.use_cassette(str(tmpdir.join("test.json"))):
|
||||
assert my_vcr.serializer == "json"
|
||||
urlopen(mockbin_request_url)
|
||||
urlopen(httpbin.url)
|
||||
|
||||
with open(str(tmpdir.join("test.json"))) as f:
|
||||
file_content = f.read()
|
||||
@@ -22,37 +22,37 @@ def test_set_serializer_default_config(tmpdir, mockbin_request_url):
|
||||
|
||||
|
||||
@pytest.mark.online
|
||||
def test_default_set_cassette_library_dir(tmpdir, mockbin_request_url):
|
||||
def test_default_set_cassette_library_dir(tmpdir, httpbin):
|
||||
my_vcr = vcr.VCR(cassette_library_dir=str(tmpdir.join("subdir")))
|
||||
|
||||
with my_vcr.use_cassette("test.json"):
|
||||
urlopen(mockbin_request_url)
|
||||
urlopen(httpbin.url)
|
||||
|
||||
assert os.path.exists(str(tmpdir.join("subdir").join("test.json")))
|
||||
|
||||
|
||||
@pytest.mark.online
|
||||
def test_override_set_cassette_library_dir(tmpdir, mockbin_request_url):
|
||||
def test_override_set_cassette_library_dir(tmpdir, httpbin):
|
||||
my_vcr = vcr.VCR(cassette_library_dir=str(tmpdir.join("subdir")))
|
||||
|
||||
cld = str(tmpdir.join("subdir2"))
|
||||
|
||||
with my_vcr.use_cassette("test.json", cassette_library_dir=cld):
|
||||
urlopen(mockbin_request_url)
|
||||
urlopen(httpbin.url)
|
||||
|
||||
assert os.path.exists(str(tmpdir.join("subdir2").join("test.json")))
|
||||
assert not os.path.exists(str(tmpdir.join("subdir").join("test.json")))
|
||||
|
||||
|
||||
@pytest.mark.online
|
||||
def test_override_match_on(tmpdir, mockbin_request_url):
|
||||
def test_override_match_on(tmpdir, httpbin):
|
||||
my_vcr = vcr.VCR(match_on=["method"])
|
||||
|
||||
with my_vcr.use_cassette(str(tmpdir.join("test.json"))):
|
||||
urlopen(mockbin_request_url)
|
||||
urlopen(httpbin.url)
|
||||
|
||||
with my_vcr.use_cassette(str(tmpdir.join("test.json"))) as cass:
|
||||
urlopen(mockbin_request_url)
|
||||
urlopen(httpbin.url)
|
||||
|
||||
assert len(cass) == 1
|
||||
assert cass.play_count == 1
|
||||
@@ -67,12 +67,12 @@ def test_missing_matcher():
|
||||
|
||||
|
||||
@pytest.mark.online
|
||||
def test_dont_record_on_exception(tmpdir, mockbin_request_url):
|
||||
def test_dont_record_on_exception(tmpdir, httpbin):
|
||||
my_vcr = vcr.VCR(record_on_exception=False)
|
||||
|
||||
@my_vcr.use_cassette(str(tmpdir.join("dontsave.yml")))
|
||||
def some_test():
|
||||
assert b"Not in content" in urlopen(mockbin_request_url)
|
||||
assert b"Not in content" in urlopen(httpbin.url)
|
||||
|
||||
with pytest.raises(AssertionError):
|
||||
some_test()
|
||||
@@ -82,6 +82,6 @@ def test_dont_record_on_exception(tmpdir, mockbin_request_url):
|
||||
# Make sure context decorator has the same behavior
|
||||
with pytest.raises(AssertionError):
|
||||
with my_vcr.use_cassette(str(tmpdir.join("dontsave2.yml"))):
|
||||
assert b"Not in content" in urlopen(mockbin_request_url).read()
|
||||
assert b"Not in content" in urlopen(httpbin.url).read()
|
||||
|
||||
assert not os.path.exists(str(tmpdir.join("dontsave2.yml")))
|
||||
|
||||
@@ -12,19 +12,19 @@ import vcr
|
||||
|
||||
|
||||
@pytest.mark.online
|
||||
def test_disk_saver_nowrite(tmpdir, mockbin_request_url):
|
||||
def test_disk_saver_nowrite(tmpdir, httpbin):
|
||||
"""
|
||||
Ensure that when you close a cassette without changing it it doesn't
|
||||
rewrite the file
|
||||
"""
|
||||
fname = str(tmpdir.join("synopsis.yaml"))
|
||||
with vcr.use_cassette(fname) as cass:
|
||||
urlopen(mockbin_request_url).read()
|
||||
urlopen(httpbin.url).read()
|
||||
assert cass.play_count == 0
|
||||
last_mod = os.path.getmtime(fname)
|
||||
|
||||
with vcr.use_cassette(fname) as cass:
|
||||
urlopen(mockbin_request_url).read()
|
||||
urlopen(httpbin.url).read()
|
||||
assert cass.play_count == 1
|
||||
assert cass.dirty is False
|
||||
last_mod2 = os.path.getmtime(fname)
|
||||
@@ -33,14 +33,14 @@ def test_disk_saver_nowrite(tmpdir, mockbin_request_url):
|
||||
|
||||
|
||||
@pytest.mark.online
|
||||
def test_disk_saver_write(tmpdir, mockbin_request_url):
|
||||
def test_disk_saver_write(tmpdir, httpbin):
|
||||
"""
|
||||
Ensure that when you close a cassette after changing it it does
|
||||
rewrite the file
|
||||
"""
|
||||
fname = str(tmpdir.join("synopsis.yaml"))
|
||||
with vcr.use_cassette(fname) as cass:
|
||||
urlopen(mockbin_request_url).read()
|
||||
urlopen(httpbin.url).read()
|
||||
assert cass.play_count == 0
|
||||
last_mod = os.path.getmtime(fname)
|
||||
|
||||
@@ -49,8 +49,8 @@ def test_disk_saver_write(tmpdir, mockbin_request_url):
|
||||
time.sleep(1)
|
||||
|
||||
with vcr.use_cassette(fname, record_mode=vcr.mode.ANY) as cass:
|
||||
urlopen(mockbin_request_url).read()
|
||||
urlopen(mockbin_request_url + "/get").read()
|
||||
urlopen(httpbin.url).read()
|
||||
urlopen(httpbin.url + "/get").read()
|
||||
assert cass.play_count == 1
|
||||
assert cass.dirty
|
||||
last_mod2 = os.path.getmtime(fname)
|
||||
|
||||
@@ -5,10 +5,11 @@ from urllib.parse import urlencode
|
||||
from urllib.request import Request, urlopen
|
||||
|
||||
import pytest
|
||||
from assertions import assert_cassette_has_one_response, assert_is_json_bytes
|
||||
|
||||
import vcr
|
||||
|
||||
from ..assertions import assert_cassette_has_one_response, assert_is_json_bytes
|
||||
|
||||
|
||||
def _request_with_auth(url, username, password):
|
||||
request = Request(url)
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
"""Integration tests with httplib2"""
|
||||
|
||||
from urllib.parse import urlencode
|
||||
|
||||
import pytest
|
||||
import pytest_httpbin.certs
|
||||
from assertions import assert_cassette_has_one_response
|
||||
|
||||
import vcr
|
||||
|
||||
from ..assertions import assert_cassette_has_one_response
|
||||
|
||||
httplib2 = pytest.importorskip("httplib2")
|
||||
|
||||
|
||||
@@ -56,14 +58,14 @@ def test_response_headers(tmpdir, httpbin_both):
|
||||
|
||||
|
||||
@pytest.mark.online
|
||||
def test_effective_url(tmpdir):
|
||||
def test_effective_url(tmpdir, httpbin):
|
||||
"""Ensure that the effective_url is captured"""
|
||||
url = "http://mockbin.org/redirect/301"
|
||||
url = httpbin.url + "/redirect-to?url=.%2F&status_code=301"
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join("headers.yaml"))):
|
||||
resp, _ = http().request(url)
|
||||
effective_url = resp["content-location"]
|
||||
assert effective_url == "http://mockbin.org/redirect/301/0"
|
||||
assert effective_url == httpbin.url + "/"
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join("headers.yaml"))):
|
||||
resp, _ = http().request(url)
|
||||
|
||||
@@ -4,10 +4,16 @@ import pytest
|
||||
|
||||
import vcr
|
||||
|
||||
from ..assertions import assert_is_json_bytes
|
||||
|
||||
asyncio = pytest.importorskip("asyncio")
|
||||
httpx = pytest.importorskip("httpx")
|
||||
|
||||
from vcr.stubs.httpx_stubs import HTTPX_REDIRECT_PARAM # noqa: E402
|
||||
|
||||
@pytest.fixture(params=["https", "http"])
|
||||
def scheme(request):
|
||||
"""Fixture that returns both http and https."""
|
||||
return request.param
|
||||
|
||||
|
||||
class BaseDoRequest:
|
||||
@@ -16,6 +22,7 @@ class BaseDoRequest:
|
||||
def __init__(self, *args, **kwargs):
|
||||
self._client_args = args
|
||||
self._client_kwargs = kwargs
|
||||
self._client_kwargs["follow_redirects"] = self._client_kwargs.get("follow_redirects", True)
|
||||
|
||||
def _make_client(self):
|
||||
return self._client_class(*self._client_args, **self._client_kwargs)
|
||||
@@ -25,22 +32,38 @@ class DoSyncRequest(BaseDoRequest):
|
||||
_client_class = httpx.Client
|
||||
|
||||
def __enter__(self):
|
||||
self._client = self._make_client()
|
||||
return self
|
||||
|
||||
def __exit__(self, *args):
|
||||
pass
|
||||
self._client.close()
|
||||
del self._client
|
||||
|
||||
@property
|
||||
def client(self):
|
||||
try:
|
||||
return self._client
|
||||
except AttributeError:
|
||||
self._client = self._make_client()
|
||||
return self._client
|
||||
except AttributeError as e:
|
||||
raise ValueError('To access sync client, use "with do_request() as client"') from e
|
||||
|
||||
def __call__(self, *args, **kwargs):
|
||||
if hasattr(self, "_client"):
|
||||
return self.client.request(*args, timeout=60, **kwargs)
|
||||
|
||||
# Use one-time context and dispose of the client afterwards
|
||||
with self:
|
||||
return self.client.request(*args, timeout=60, **kwargs)
|
||||
|
||||
def stream(self, *args, **kwargs):
|
||||
if hasattr(self, "_client"):
|
||||
with self.client.stream(*args, **kwargs) as response:
|
||||
return b"".join(response.iter_bytes())
|
||||
|
||||
# Use one-time context and dispose of the client afterwards
|
||||
with self:
|
||||
with self.client.stream(*args, **kwargs) as response:
|
||||
return b"".join(response.iter_bytes())
|
||||
|
||||
|
||||
class DoAsyncRequest(BaseDoRequest):
|
||||
_client_class = httpx.AsyncClient
|
||||
@@ -75,7 +98,22 @@ class DoAsyncRequest(BaseDoRequest):
|
||||
|
||||
# Use one-time context and dispose of the loop/client afterwards
|
||||
with self:
|
||||
return self(*args, **kwargs)
|
||||
return self._loop.run_until_complete(self.client.request(*args, **kwargs))
|
||||
|
||||
async def _get_stream(self, *args, **kwargs):
|
||||
async with self.client.stream(*args, **kwargs) as response:
|
||||
content = b""
|
||||
async for c in response.aiter_bytes():
|
||||
content += c
|
||||
return content
|
||||
|
||||
def stream(self, *args, **kwargs):
|
||||
if hasattr(self, "_loop"):
|
||||
return self._loop.run_until_complete(self._get_stream(*args, **kwargs))
|
||||
|
||||
# Use one-time context and dispose of the loop/client afterwards
|
||||
with self:
|
||||
return self._loop.run_until_complete(self._get_stream(*args, **kwargs))
|
||||
|
||||
|
||||
def pytest_generate_tests(metafunc):
|
||||
@@ -89,8 +127,8 @@ def yml(tmpdir, request):
|
||||
|
||||
|
||||
@pytest.mark.online
|
||||
def test_status(tmpdir, mockbin, do_request):
|
||||
url = mockbin
|
||||
def test_status(tmpdir, httpbin, do_request):
|
||||
url = httpbin.url
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join("status.yaml"))):
|
||||
response = do_request()("GET", url)
|
||||
@@ -102,8 +140,8 @@ def test_status(tmpdir, mockbin, do_request):
|
||||
|
||||
|
||||
@pytest.mark.online
|
||||
def test_case_insensitive_headers(tmpdir, mockbin, do_request):
|
||||
url = mockbin
|
||||
def test_case_insensitive_headers(tmpdir, httpbin, do_request):
|
||||
url = httpbin.url
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join("whatever.yaml"))):
|
||||
do_request()("GET", url)
|
||||
@@ -116,8 +154,8 @@ def test_case_insensitive_headers(tmpdir, mockbin, do_request):
|
||||
|
||||
|
||||
@pytest.mark.online
|
||||
def test_content(tmpdir, mockbin, do_request):
|
||||
url = mockbin
|
||||
def test_content(tmpdir, httpbin, do_request):
|
||||
url = httpbin.url
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join("cointent.yaml"))):
|
||||
response = do_request()("GET", url)
|
||||
@@ -129,23 +167,21 @@ def test_content(tmpdir, mockbin, do_request):
|
||||
|
||||
|
||||
@pytest.mark.online
|
||||
def test_json(tmpdir, mockbin, do_request):
|
||||
url = mockbin + "/request"
|
||||
|
||||
headers = {"content-type": "application/json"}
|
||||
def test_json(tmpdir, httpbin, do_request):
|
||||
url = httpbin.url + "/json"
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join("json.yaml"))):
|
||||
response = do_request(headers=headers)("GET", url)
|
||||
response = do_request()("GET", url)
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join("json.yaml"))) as cassette:
|
||||
cassette_response = do_request(headers=headers)("GET", url)
|
||||
cassette_response = do_request()("GET", url)
|
||||
assert cassette_response.json() == response.json()
|
||||
assert cassette.play_count == 1
|
||||
|
||||
|
||||
@pytest.mark.online
|
||||
def test_params_same_url_distinct_params(tmpdir, mockbin, do_request):
|
||||
url = mockbin + "/request"
|
||||
def test_params_same_url_distinct_params(tmpdir, httpbin, do_request):
|
||||
url = httpbin.url + "/get"
|
||||
headers = {"Content-Type": "application/json"}
|
||||
params = {"a": 1, "b": False, "c": "c"}
|
||||
|
||||
@@ -165,22 +201,20 @@ def test_params_same_url_distinct_params(tmpdir, mockbin, do_request):
|
||||
|
||||
|
||||
@pytest.mark.online
|
||||
def test_redirect(mockbin, yml, do_request):
|
||||
url = mockbin + "/redirect/303/2"
|
||||
def test_redirect(httpbin, yml, do_request):
|
||||
url = httpbin.url + "/redirect-to"
|
||||
|
||||
redirect_kwargs = {HTTPX_REDIRECT_PARAM.name: True}
|
||||
|
||||
response = do_request()("GET", url, **redirect_kwargs)
|
||||
response = do_request()("GET", url)
|
||||
with vcr.use_cassette(yml):
|
||||
response = do_request()("GET", url, **redirect_kwargs)
|
||||
response = do_request()("GET", url, params={"url": "./get", "status_code": 302})
|
||||
|
||||
with vcr.use_cassette(yml) as cassette:
|
||||
cassette_response = do_request()("GET", url, **redirect_kwargs)
|
||||
cassette_response = do_request()("GET", url, params={"url": "./get", "status_code": 302})
|
||||
|
||||
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 len(cassette) == 2
|
||||
assert cassette.play_count == 2
|
||||
|
||||
# Assert that the real response and the cassette response have a similar
|
||||
# looking request_info.
|
||||
@@ -189,22 +223,6 @@ def test_redirect(mockbin, yml, do_request):
|
||||
assert cassette_response.request.headers.items() == response.request.headers.items()
|
||||
|
||||
|
||||
@pytest.mark.online
|
||||
def test_work_with_gzipped_data(mockbin, do_request, yml):
|
||||
url = mockbin + "/gzip?foo=bar"
|
||||
headers = {"accept-encoding": "deflate, gzip"}
|
||||
|
||||
with vcr.use_cassette(yml):
|
||||
do_request(headers=headers)("GET", url)
|
||||
|
||||
with vcr.use_cassette(yml) as cassette:
|
||||
cassette_response = do_request(headers=headers)("GET", url)
|
||||
|
||||
assert cassette_response.headers["content-encoding"] == "gzip"
|
||||
assert cassette_response.read()
|
||||
assert cassette.play_count == 1
|
||||
|
||||
|
||||
@pytest.mark.online
|
||||
@pytest.mark.parametrize("url", ["https://github.com/kevin1024/vcrpy/issues/" + str(i) for i in range(3, 6)])
|
||||
def test_simple_fetching(do_request, yml, url):
|
||||
@@ -217,55 +235,32 @@ def test_simple_fetching(do_request, yml, url):
|
||||
assert cassette.play_count == 1
|
||||
|
||||
|
||||
def test_behind_proxy(do_request):
|
||||
# This is recorded because otherwise we should have a live proxy somewhere.
|
||||
yml = (
|
||||
os.path.dirname(os.path.realpath(__file__)) + "/cassettes/" + "test_httpx_test_test_behind_proxy.yml"
|
||||
)
|
||||
url = "https://mockbin.org/headers"
|
||||
proxy = "http://localhost:8080"
|
||||
proxies = {"http://": proxy, "https://": proxy}
|
||||
|
||||
with vcr.use_cassette(yml):
|
||||
response = do_request(proxies=proxies, verify=False)("GET", url)
|
||||
|
||||
with vcr.use_cassette(yml) as cassette:
|
||||
cassette_response = do_request(proxies=proxies, verify=False)("GET", url)
|
||||
assert str(cassette_response.request.url) == url
|
||||
assert cassette.play_count == 1
|
||||
|
||||
assert cassette_response.headers["Via"] == "my_own_proxy", str(cassette_response.headers)
|
||||
assert cassette_response.request.url == response.request.url
|
||||
|
||||
|
||||
@pytest.mark.online
|
||||
def test_cookies(tmpdir, mockbin, do_request):
|
||||
def test_cookies(tmpdir, httpbin, do_request):
|
||||
def client_cookies(client):
|
||||
return list(client.client.cookies)
|
||||
|
||||
def response_cookies(response):
|
||||
return list(response.cookies)
|
||||
|
||||
url = mockbin + "/bin/26148652-fe25-4f21-aaf5-689b5b4bf65f"
|
||||
headers = {"cookie": "k1=v1;k2=v2"}
|
||||
url = httpbin.url + "/cookies/set"
|
||||
params = {"k1": "v1", "k2": "v2"}
|
||||
|
||||
with do_request(headers=headers) as client:
|
||||
with do_request(params=params, follow_redirects=False) as client:
|
||||
assert client_cookies(client) == []
|
||||
|
||||
redirect_kwargs = {HTTPX_REDIRECT_PARAM.name: True}
|
||||
|
||||
testfile = str(tmpdir.join("cookies.yml"))
|
||||
with vcr.use_cassette(testfile):
|
||||
r1 = client("GET", url, **redirect_kwargs)
|
||||
r1 = client("GET", url)
|
||||
|
||||
assert response_cookies(r1) == ["k1", "k2"]
|
||||
|
||||
r2 = client("GET", url, **redirect_kwargs)
|
||||
r2 = client("GET", url)
|
||||
|
||||
assert response_cookies(r2) == ["k1", "k2"]
|
||||
assert client_cookies(client) == ["k1", "k2"]
|
||||
|
||||
with do_request(headers=headers) as new_client:
|
||||
with do_request(params=params, follow_redirects=False) as new_client:
|
||||
assert client_cookies(new_client) == []
|
||||
|
||||
with vcr.use_cassette(testfile) as cassette:
|
||||
@@ -277,40 +272,90 @@ def test_cookies(tmpdir, mockbin, do_request):
|
||||
|
||||
|
||||
@pytest.mark.online
|
||||
def test_relative_redirects(tmpdir, scheme, do_request, mockbin):
|
||||
redirect_kwargs = {HTTPX_REDIRECT_PARAM.name: True}
|
||||
def test_stream(tmpdir, httpbin, do_request):
|
||||
url = httpbin.url + "/stream-bytes/512"
|
||||
testfile = str(tmpdir.join("stream.yml"))
|
||||
|
||||
url = mockbin + "/redirect/301?to=/redirect/301?to=/request"
|
||||
testfile = str(tmpdir.join("relative_redirects.yml"))
|
||||
with vcr.use_cassette(testfile):
|
||||
response = do_request()("GET", url, **redirect_kwargs)
|
||||
assert len(response.history) == 2, response
|
||||
assert response.json()["url"].endswith("request")
|
||||
response_content = do_request().stream("GET", url)
|
||||
assert len(response_content) == 512
|
||||
|
||||
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
|
||||
cassette_content = do_request().stream("GET", url)
|
||||
assert cassette_content == response_content
|
||||
assert len(cassette_content) == 512
|
||||
assert cassette.play_count == 1
|
||||
|
||||
|
||||
@pytest.mark.online
|
||||
def test_redirect_wo_allow_redirects(do_request, mockbin, yml):
|
||||
url = mockbin + "/redirect/308/5"
|
||||
|
||||
redirect_kwargs = {HTTPX_REDIRECT_PARAM.name: False}
|
||||
|
||||
with vcr.use_cassette(yml):
|
||||
response = do_request()("GET", url, **redirect_kwargs)
|
||||
|
||||
assert str(response.url).endswith("308/5")
|
||||
assert response.status_code == 308
|
||||
# Regular cassette formats support the status reason,
|
||||
# but the old HTTPX cassette format does not.
|
||||
@pytest.mark.parametrize(
|
||||
"cassette_name,reason",
|
||||
[
|
||||
("requests", "great"),
|
||||
("httpx_old_format", "OK"),
|
||||
],
|
||||
)
|
||||
def test_load_cassette_format(do_request, cassette_name, reason):
|
||||
mydir = os.path.dirname(os.path.realpath(__file__))
|
||||
yml = f"{mydir}/cassettes/gzip_{cassette_name}.yaml"
|
||||
url = "https://httpbin.org/gzip"
|
||||
|
||||
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
|
||||
|
||||
cassette_response = do_request()("GET", url)
|
||||
assert str(cassette_response.request.url) == url
|
||||
assert cassette.play_count == 1
|
||||
|
||||
# Should be able to load up the JSON inside,
|
||||
# regardless whether the content is the gzipped
|
||||
# in the cassette or not.
|
||||
json = cassette_response.json()
|
||||
assert json["method"] == "GET", json
|
||||
assert cassette_response.status_code == 200
|
||||
assert cassette_response.reason_phrase == reason
|
||||
|
||||
|
||||
def test_gzip__decode_compressed_response_false(tmpdir, httpbin, do_request):
|
||||
"""
|
||||
Ensure that httpx is able to automatically decompress the response body.
|
||||
"""
|
||||
for _ in range(2): # one for recording, one for re-playing
|
||||
with vcr.use_cassette(str(tmpdir.join("gzip.yaml"))) as cassette:
|
||||
response = do_request()("GET", httpbin + "/gzip")
|
||||
assert response.headers["content-encoding"] == "gzip" # i.e. not removed
|
||||
# The content stored in the cassette should be gzipped.
|
||||
assert cassette.responses[0]["body"]["string"][:2] == b"\x1f\x8b"
|
||||
assert_is_json_bytes(response.content) # i.e. uncompressed bytes
|
||||
|
||||
|
||||
def test_gzip__decode_compressed_response_true(do_request, tmpdir, httpbin):
|
||||
url = httpbin + "/gzip"
|
||||
|
||||
expected_response = do_request()("GET", url)
|
||||
expected_content = expected_response.content
|
||||
assert expected_response.headers["content-encoding"] == "gzip" # self-test
|
||||
|
||||
with vcr.use_cassette(
|
||||
str(tmpdir.join("decode_compressed.yaml")),
|
||||
decode_compressed_response=True,
|
||||
) as cassette:
|
||||
r = do_request()("GET", url)
|
||||
assert r.headers["content-encoding"] == "gzip" # i.e. not removed
|
||||
content_length = r.headers["content-length"]
|
||||
assert r.content == expected_content
|
||||
|
||||
# Has the cassette body been decompressed?
|
||||
cassette_response_body = cassette.responses[0]["body"]["string"]
|
||||
assert isinstance(cassette_response_body, str)
|
||||
|
||||
# Content should be JSON.
|
||||
assert cassette_response_body[0:1] == "{"
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join("decode_compressed.yaml")), decode_compressed_response=True):
|
||||
r = httpx.get(url)
|
||||
assert "content-encoding" not in r.headers # i.e. removed
|
||||
assert r.content == expected_content
|
||||
|
||||
# As the content is uncompressed, it should have a bigger
|
||||
# length than the compressed version.
|
||||
assert r.headers["content-length"] > content_length
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
"""Test using a proxy."""
|
||||
|
||||
import http.server
|
||||
import multiprocessing
|
||||
import socketserver
|
||||
import threading
|
||||
from urllib.request import urlopen
|
||||
|
||||
import pytest
|
||||
@@ -29,7 +29,8 @@ class Proxy(http.server.SimpleHTTPRequestHandler):
|
||||
# In Python 2 the response is an addinfourl instance.
|
||||
status = upstream_response.code
|
||||
headers = upstream_response.info().items()
|
||||
self.send_response(status, upstream_response.msg)
|
||||
self.log_request(status)
|
||||
self.send_response_only(status, upstream_response.msg)
|
||||
for header in headers:
|
||||
self.send_header(*header)
|
||||
self.end_headers()
|
||||
@@ -38,11 +39,12 @@ class Proxy(http.server.SimpleHTTPRequestHandler):
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def proxy_server():
|
||||
httpd = socketserver.ThreadingTCPServer(("", 0), Proxy)
|
||||
proxy_process = multiprocessing.Process(target=httpd.serve_forever)
|
||||
with socketserver.ThreadingTCPServer(("", 0), Proxy) as httpd:
|
||||
proxy_process = threading.Thread(target=httpd.serve_forever)
|
||||
proxy_process.start()
|
||||
yield "http://{}:{}".format(*httpd.server_address)
|
||||
proxy_process.terminate()
|
||||
httpd.shutdown()
|
||||
proxy_process.join()
|
||||
|
||||
|
||||
def test_use_proxy(tmpdir, httpbin, proxy_server):
|
||||
@@ -50,8 +52,10 @@ def test_use_proxy(tmpdir, httpbin, proxy_server):
|
||||
with vcr.use_cassette(str(tmpdir.join("proxy.yaml"))):
|
||||
response = requests.get(httpbin.url, proxies={"http": proxy_server})
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join("proxy.yaml"))) as cassette:
|
||||
with vcr.use_cassette(str(tmpdir.join("proxy.yaml")), mode="once") as cassette:
|
||||
cassette_response = requests.get(httpbin.url, proxies={"http": proxy_server})
|
||||
|
||||
for key in set(cassette_response.headers.keys()) & set(response.headers.keys()):
|
||||
assert cassette_response.headers[key] == response.headers[key]
|
||||
assert cassette_response.headers == response.headers
|
||||
assert cassette.play_count == 1
|
||||
|
||||
@@ -14,28 +14,28 @@ def false_matcher(r1, r2):
|
||||
|
||||
|
||||
@pytest.mark.online
|
||||
def test_registered_true_matcher(tmpdir, mockbin_request_url):
|
||||
def test_registered_true_matcher(tmpdir, httpbin):
|
||||
my_vcr = vcr.VCR()
|
||||
my_vcr.register_matcher("true", true_matcher)
|
||||
testfile = str(tmpdir.join("test.yml"))
|
||||
with my_vcr.use_cassette(testfile, match_on=["true"]):
|
||||
# These 2 different urls are stored as the same request
|
||||
urlopen(mockbin_request_url)
|
||||
urlopen(mockbin_request_url + "/get")
|
||||
urlopen(httpbin.url)
|
||||
urlopen(httpbin.url + "/get")
|
||||
|
||||
with my_vcr.use_cassette(testfile, match_on=["true"]):
|
||||
# I can get the response twice even though I only asked for it once
|
||||
urlopen(mockbin_request_url)
|
||||
urlopen(mockbin_request_url)
|
||||
urlopen(httpbin.url)
|
||||
urlopen(httpbin.url)
|
||||
|
||||
|
||||
@pytest.mark.online
|
||||
def test_registered_false_matcher(tmpdir, mockbin_request_url):
|
||||
def test_registered_false_matcher(tmpdir, httpbin):
|
||||
my_vcr = vcr.VCR()
|
||||
my_vcr.register_matcher("false", false_matcher)
|
||||
testfile = str(tmpdir.join("test.yml"))
|
||||
with my_vcr.use_cassette(testfile, match_on=["false"]) as cass:
|
||||
# These 2 different urls are stored as different requests
|
||||
urlopen(mockbin_request_url)
|
||||
urlopen(mockbin_request_url + "/get")
|
||||
urlopen(httpbin.url)
|
||||
urlopen(httpbin.url + "/get")
|
||||
assert len(cass) == 2
|
||||
|
||||
@@ -66,7 +66,7 @@ def test_load_cassette_with_custom_persister(tmpdir, httpbin):
|
||||
|
||||
with my_vcr.use_cassette(test_fixture, serializer="json"):
|
||||
response = urlopen(httpbin.url).read()
|
||||
assert b"difficult sometimes" in response
|
||||
assert b"HTTP Request & Response Service" in response
|
||||
|
||||
|
||||
def test_load_cassette_persister_exception_handling(tmpdir, httpbin):
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
"""Test requests' interaction with vcr"""
|
||||
|
||||
import pytest
|
||||
from assertions import assert_cassette_empty, assert_is_json_bytes
|
||||
|
||||
import vcr
|
||||
|
||||
from ..assertions import assert_cassette_empty, assert_is_json_bytes
|
||||
|
||||
requests = pytest.importorskip("requests")
|
||||
|
||||
|
||||
@@ -264,7 +266,7 @@ def test_nested_cassettes_with_session_created_before_nesting(httpbin_both, tmpd
|
||||
def test_post_file(tmpdir, httpbin_both):
|
||||
"""Ensure that we handle posting a file."""
|
||||
url = httpbin_both + "/post"
|
||||
with vcr.use_cassette(str(tmpdir.join("post_file.yaml"))) as cass, open("tox.ini", "rb") as f:
|
||||
with vcr.use_cassette(str(tmpdir.join("post_file.yaml"))) as cass, open(".editorconfig", "rb") as f:
|
||||
original_response = requests.post(url, f).content
|
||||
|
||||
# This also tests that we do the right thing with matching the body when they are files.
|
||||
@@ -272,10 +274,10 @@ def test_post_file(tmpdir, httpbin_both):
|
||||
str(tmpdir.join("post_file.yaml")),
|
||||
match_on=("method", "scheme", "host", "port", "path", "query", "body"),
|
||||
) as cass:
|
||||
with open("tox.ini", "rb") as f:
|
||||
tox_content = f.read()
|
||||
assert cass.requests[0].body.read() == tox_content
|
||||
with open("tox.ini", "rb") as f:
|
||||
with open(".editorconfig", "rb") as f:
|
||||
editorconfig = f.read()
|
||||
assert cass.requests[0].body.read() == editorconfig
|
||||
with open(".editorconfig", "rb") as f:
|
||||
new_response = requests.post(url, f).content
|
||||
assert original_response == new_response
|
||||
|
||||
|
||||
@@ -2,10 +2,10 @@ import http.client as httplib
|
||||
import json
|
||||
import zlib
|
||||
|
||||
from assertions import assert_is_json_bytes
|
||||
|
||||
import vcr
|
||||
|
||||
from ..assertions import assert_is_json_bytes
|
||||
|
||||
|
||||
def _headers_are_case_insensitive(host, port):
|
||||
conn = httplib.HTTPConnection(host, port)
|
||||
|
||||
@@ -1,19 +1,45 @@
|
||||
"""Test requests' interaction with vcr"""
|
||||
|
||||
import asyncio
|
||||
import functools
|
||||
import inspect
|
||||
import json
|
||||
|
||||
import pytest
|
||||
from assertions import assert_cassette_empty, assert_is_json_bytes
|
||||
|
||||
import vcr
|
||||
from vcr.errors import CannotOverwriteExistingCassetteException
|
||||
|
||||
from ..assertions import assert_cassette_empty, assert_is_json_bytes
|
||||
|
||||
tornado = pytest.importorskip("tornado")
|
||||
gen = pytest.importorskip("tornado.gen")
|
||||
http = pytest.importorskip("tornado.httpclient")
|
||||
|
||||
# whether the current version of Tornado supports the raise_error argument for
|
||||
# fetch().
|
||||
supports_raise_error = tornado.version_info >= (4,)
|
||||
raise_error_for_response_code_only = tornado.version_info >= (6,)
|
||||
|
||||
|
||||
def gen_test(func):
|
||||
@functools.wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
async def coro():
|
||||
return await gen.coroutine(func)(*args, **kwargs)
|
||||
|
||||
return asyncio.run(coro())
|
||||
|
||||
# Patch the signature so pytest can inject fixtures
|
||||
# we can't use wrapt.decorator because it returns a generator function
|
||||
wrapper.__signature__ = inspect.signature(func)
|
||||
return wrapper
|
||||
|
||||
|
||||
@pytest.fixture(params=["https", "http"])
|
||||
def scheme(request):
|
||||
"""Fixture that returns both http and https."""
|
||||
return request.param
|
||||
|
||||
|
||||
@pytest.fixture(params=["simple", "curl", "default"])
|
||||
@@ -43,7 +69,8 @@ def post(client, url, data=None, **kwargs):
|
||||
return client.fetch(http.HTTPRequest(url, method="POST", **kwargs))
|
||||
|
||||
|
||||
@pytest.mark.gen_test
|
||||
@pytest.mark.online
|
||||
@gen_test
|
||||
def test_status_code(get_client, scheme, tmpdir):
|
||||
"""Ensure that we can read the status code"""
|
||||
url = scheme + "://httpbin.org/"
|
||||
@@ -55,7 +82,8 @@ def test_status_code(get_client, scheme, tmpdir):
|
||||
assert 1 == cass.play_count
|
||||
|
||||
|
||||
@pytest.mark.gen_test
|
||||
@pytest.mark.online
|
||||
@gen_test
|
||||
def test_headers(get_client, scheme, tmpdir):
|
||||
"""Ensure that we can read the headers back"""
|
||||
url = scheme + "://httpbin.org/"
|
||||
@@ -67,7 +95,8 @@ def test_headers(get_client, scheme, tmpdir):
|
||||
assert 1 == cass.play_count
|
||||
|
||||
|
||||
@pytest.mark.gen_test
|
||||
@pytest.mark.online
|
||||
@gen_test
|
||||
def test_body(get_client, tmpdir, scheme):
|
||||
"""Ensure the responses are all identical enough"""
|
||||
|
||||
@@ -80,20 +109,21 @@ def test_body(get_client, tmpdir, scheme):
|
||||
assert 1 == cass.play_count
|
||||
|
||||
|
||||
@pytest.mark.gen_test
|
||||
def test_effective_url(get_client, scheme, tmpdir):
|
||||
@gen_test
|
||||
def test_effective_url(get_client, tmpdir, httpbin):
|
||||
"""Ensure that the effective_url is captured"""
|
||||
url = scheme + "://mockbin.org/redirect/301?url=/html"
|
||||
url = httpbin.url + "/redirect/1"
|
||||
with vcr.use_cassette(str(tmpdir.join("url.yaml"))):
|
||||
effective_url = (yield get(get_client(), url)).effective_url
|
||||
assert effective_url == scheme + "://mockbin.org/redirect/301/0"
|
||||
assert effective_url == httpbin.url + "/get"
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join("url.yaml"))) as cass:
|
||||
assert effective_url == (yield get(get_client(), url)).effective_url
|
||||
assert 1 == cass.play_count
|
||||
|
||||
|
||||
@pytest.mark.gen_test
|
||||
@pytest.mark.online
|
||||
@gen_test
|
||||
def test_auth(get_client, tmpdir, scheme):
|
||||
"""Ensure that we can handle basic auth"""
|
||||
auth = ("user", "passwd")
|
||||
@@ -108,7 +138,8 @@ def test_auth(get_client, tmpdir, scheme):
|
||||
assert 1 == cass.play_count
|
||||
|
||||
|
||||
@pytest.mark.gen_test
|
||||
@pytest.mark.online
|
||||
@gen_test
|
||||
def test_auth_failed(get_client, tmpdir, scheme):
|
||||
"""Ensure that we can save failed auth statuses"""
|
||||
auth = ("user", "wrongwrongwrong")
|
||||
@@ -131,7 +162,8 @@ def test_auth_failed(get_client, tmpdir, scheme):
|
||||
assert 1 == cass.play_count
|
||||
|
||||
|
||||
@pytest.mark.gen_test
|
||||
@pytest.mark.online
|
||||
@gen_test
|
||||
def test_post(get_client, tmpdir, scheme):
|
||||
"""Ensure that we can post and cache the results"""
|
||||
data = {"key1": "value1", "key2": "value2"}
|
||||
@@ -146,10 +178,10 @@ def test_post(get_client, tmpdir, scheme):
|
||||
assert 1 == cass.play_count
|
||||
|
||||
|
||||
@pytest.mark.gen_test
|
||||
def test_redirects(get_client, tmpdir, scheme):
|
||||
@gen_test
|
||||
def test_redirects(get_client, tmpdir, httpbin):
|
||||
"""Ensure that we can handle redirects"""
|
||||
url = scheme + "://mockbin.org/redirect/301?url=bytes/1024"
|
||||
url = httpbin + "/redirect-to?url=bytes/1024&status_code=301"
|
||||
with vcr.use_cassette(str(tmpdir.join("requests.yaml"))):
|
||||
content = (yield get(get_client(), url)).body
|
||||
|
||||
@@ -158,7 +190,8 @@ def test_redirects(get_client, tmpdir, scheme):
|
||||
assert cass.play_count == 1
|
||||
|
||||
|
||||
@pytest.mark.gen_test
|
||||
@pytest.mark.online
|
||||
@gen_test
|
||||
def test_cross_scheme(get_client, tmpdir, scheme):
|
||||
"""Ensure that requests between schemes are treated separately"""
|
||||
# First fetch a url under http, and then again under https and then
|
||||
@@ -177,7 +210,8 @@ def test_cross_scheme(get_client, tmpdir, scheme):
|
||||
assert cass.play_count == 2
|
||||
|
||||
|
||||
@pytest.mark.gen_test
|
||||
@pytest.mark.online
|
||||
@gen_test
|
||||
def test_gzip(get_client, tmpdir, scheme):
|
||||
"""
|
||||
Ensure that httpclient is able to automatically decompress the response
|
||||
@@ -202,7 +236,8 @@ def test_gzip(get_client, tmpdir, scheme):
|
||||
assert 1 == cass.play_count
|
||||
|
||||
|
||||
@pytest.mark.gen_test
|
||||
@pytest.mark.online
|
||||
@gen_test
|
||||
def test_https_with_cert_validation_disabled(get_client, tmpdir):
|
||||
cass_path = str(tmpdir.join("cert_validation_disabled.yaml"))
|
||||
|
||||
@@ -214,7 +249,7 @@ def test_https_with_cert_validation_disabled(get_client, tmpdir):
|
||||
assert 1 == cass.play_count
|
||||
|
||||
|
||||
@pytest.mark.gen_test
|
||||
@gen_test
|
||||
def test_unsupported_features_raises_in_future(get_client, tmpdir):
|
||||
"""Ensure that the exception for an AsyncHTTPClient feature not being
|
||||
supported is raised inside the future."""
|
||||
@@ -232,7 +267,11 @@ def test_unsupported_features_raises_in_future(get_client, tmpdir):
|
||||
|
||||
|
||||
@pytest.mark.skipif(not supports_raise_error, reason="raise_error unavailable in tornado <= 3")
|
||||
@pytest.mark.gen_test
|
||||
@pytest.mark.skipif(
|
||||
raise_error_for_response_code_only,
|
||||
reason="raise_error only ignores HTTPErrors due to response code",
|
||||
)
|
||||
@gen_test
|
||||
def test_unsupported_features_raise_error_disabled(get_client, tmpdir):
|
||||
"""Ensure that the exception for an AsyncHTTPClient feature not being
|
||||
supported is not raised if raise_error=False."""
|
||||
@@ -251,7 +290,8 @@ def test_unsupported_features_raise_error_disabled(get_client, tmpdir):
|
||||
assert "not yet supported by VCR" in str(response.error)
|
||||
|
||||
|
||||
@pytest.mark.gen_test
|
||||
@pytest.mark.online
|
||||
@gen_test
|
||||
def test_cannot_overwrite_cassette_raises_in_future(get_client, tmpdir):
|
||||
"""Ensure that CannotOverwriteExistingCassetteException is raised inside
|
||||
the future."""
|
||||
@@ -267,7 +307,11 @@ def test_cannot_overwrite_cassette_raises_in_future(get_client, tmpdir):
|
||||
|
||||
|
||||
@pytest.mark.skipif(not supports_raise_error, reason="raise_error unavailable in tornado <= 3")
|
||||
@pytest.mark.gen_test
|
||||
@pytest.mark.skipif(
|
||||
raise_error_for_response_code_only,
|
||||
reason="raise_error only ignores HTTPErrors due to response code",
|
||||
)
|
||||
@gen_test
|
||||
def test_cannot_overwrite_cassette_raise_error_disabled(get_client, tmpdir):
|
||||
"""Ensure that CannotOverwriteExistingCassetteException is not raised if
|
||||
raise_error=False in the fetch() call."""
|
||||
@@ -281,14 +325,14 @@ def test_cannot_overwrite_cassette_raise_error_disabled(get_client, tmpdir):
|
||||
assert isinstance(response.error, CannotOverwriteExistingCassetteException)
|
||||
|
||||
|
||||
@pytest.mark.gen_test
|
||||
@gen_test
|
||||
@vcr.use_cassette(path_transformer=vcr.default_vcr.ensure_suffix(".yaml"))
|
||||
def test_tornado_with_decorator_use_cassette(get_client):
|
||||
response = yield get_client().fetch(http.HTTPRequest("http://www.google.com/", method="GET"))
|
||||
assert response.body.decode("utf-8") == "not actually google"
|
||||
|
||||
|
||||
@pytest.mark.gen_test
|
||||
@gen_test
|
||||
@vcr.use_cassette(path_transformer=vcr.default_vcr.ensure_suffix(".yaml"))
|
||||
def test_tornado_exception_can_be_caught(get_client):
|
||||
try:
|
||||
@@ -302,7 +346,8 @@ def test_tornado_exception_can_be_caught(get_client):
|
||||
assert e.code == 404
|
||||
|
||||
|
||||
@pytest.mark.gen_test
|
||||
@pytest.mark.online
|
||||
@gen_test
|
||||
def test_existing_references_get_patched(tmpdir):
|
||||
from tornado.httpclient import AsyncHTTPClient
|
||||
|
||||
@@ -315,7 +360,8 @@ def test_existing_references_get_patched(tmpdir):
|
||||
assert cass.play_count == 1
|
||||
|
||||
|
||||
@pytest.mark.gen_test
|
||||
@pytest.mark.online
|
||||
@gen_test
|
||||
def test_existing_instances_get_patched(get_client, tmpdir):
|
||||
"""Ensure that existing instances of AsyncHTTPClient get patched upon
|
||||
entering VCR context."""
|
||||
@@ -330,7 +376,8 @@ def test_existing_instances_get_patched(get_client, tmpdir):
|
||||
assert cass.play_count == 1
|
||||
|
||||
|
||||
@pytest.mark.gen_test
|
||||
@pytest.mark.online
|
||||
@gen_test
|
||||
def test_request_time_is_set(get_client, tmpdir):
|
||||
"""Ensures that the request_time on HTTPResponses is set."""
|
||||
|
||||
|
||||
@@ -5,12 +5,13 @@ from urllib.parse import urlencode
|
||||
from urllib.request import urlopen
|
||||
|
||||
import pytest_httpbin.certs
|
||||
from assertions import assert_cassette_has_one_response
|
||||
from pytest import mark
|
||||
|
||||
# Internal imports
|
||||
import vcr
|
||||
|
||||
from ..assertions import assert_cassette_has_one_response
|
||||
|
||||
|
||||
def urlopen_with_cafile(*args, **kwargs):
|
||||
context = ssl.create_default_context(cafile=pytest_httpbin.certs.where())
|
||||
@@ -57,13 +58,13 @@ def test_response_headers(httpbin_both, tmpdir):
|
||||
|
||||
|
||||
@mark.online
|
||||
def test_effective_url(tmpdir):
|
||||
def test_effective_url(tmpdir, httpbin):
|
||||
"""Ensure that the effective_url is captured"""
|
||||
url = "http://mockbin.org/redirect/301"
|
||||
url = httpbin.url + "/redirect-to?url=.%2F&status_code=301"
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join("headers.yaml"))):
|
||||
effective_url = urlopen_with_cafile(url).geturl()
|
||||
assert effective_url == "http://mockbin.org/redirect/301/0"
|
||||
assert effective_url == httpbin.url + "/"
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join("headers.yaml"))):
|
||||
assert effective_url == urlopen_with_cafile(url).geturl()
|
||||
|
||||
@@ -4,12 +4,13 @@
|
||||
|
||||
import pytest
|
||||
import pytest_httpbin
|
||||
from assertions import assert_cassette_empty, assert_is_json_bytes
|
||||
|
||||
import vcr
|
||||
from vcr.patch import force_reset
|
||||
from vcr.stubs.compat import get_headers
|
||||
|
||||
from ..assertions import assert_cassette_empty, assert_is_json_bytes
|
||||
|
||||
urllib3 = pytest.importorskip("urllib3")
|
||||
|
||||
|
||||
@@ -99,9 +100,9 @@ def test_post(tmpdir, httpbin_both, verify_pool_mgr):
|
||||
|
||||
|
||||
@pytest.mark.online
|
||||
def test_redirects(tmpdir, verify_pool_mgr):
|
||||
def test_redirects(tmpdir, verify_pool_mgr, httpbin):
|
||||
"""Ensure that we can handle redirects"""
|
||||
url = "http://mockbin.org/redirect/301"
|
||||
url = httpbin.url + "/redirect/1"
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join("verify_pool_mgr.yaml"))):
|
||||
content = verify_pool_mgr.request("GET", url).data
|
||||
|
||||
@@ -63,7 +63,7 @@ def test_flickr_should_respond_with_200(tmpdir):
|
||||
def test_cookies(tmpdir, httpbin):
|
||||
testfile = str(tmpdir.join("cookies.yml"))
|
||||
with vcr.use_cassette(testfile):
|
||||
s = requests.Session()
|
||||
with requests.Session() as s:
|
||||
s.get(httpbin.url + "/cookies/set?k1=v1&k2=v2")
|
||||
assert s.cookies.keys() == ["k1", "k2"]
|
||||
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import contextlib
|
||||
import http.client as httplib
|
||||
from io import BytesIO
|
||||
from tempfile import NamedTemporaryFile
|
||||
from unittest import mock
|
||||
|
||||
from pytest import mark
|
||||
|
||||
from vcr import mode
|
||||
from vcr import mode, use_cassette
|
||||
from vcr.cassette import Cassette
|
||||
from vcr.stubs import VCRHTTPSConnection
|
||||
|
||||
@@ -16,7 +20,56 @@ class TestVCRConnection:
|
||||
@mark.online
|
||||
@mock.patch("vcr.cassette.Cassette.can_play_response_for", return_value=False)
|
||||
def testing_connect(*args):
|
||||
vcr_connection = VCRHTTPSConnection("www.google.com")
|
||||
with contextlib.closing(VCRHTTPSConnection("www.google.com")) as vcr_connection:
|
||||
vcr_connection.cassette = Cassette("test", record_mode=mode.ALL)
|
||||
vcr_connection.real_connection.connect()
|
||||
assert vcr_connection.real_connection.sock is not None
|
||||
|
||||
def test_body_consumed_once_stream(self, tmpdir, httpbin):
|
||||
self._test_body_consumed_once(
|
||||
tmpdir,
|
||||
httpbin,
|
||||
BytesIO(b"1234567890"),
|
||||
BytesIO(b"9876543210"),
|
||||
BytesIO(b"9876543210"),
|
||||
)
|
||||
|
||||
def test_body_consumed_once_iterator(self, tmpdir, httpbin):
|
||||
self._test_body_consumed_once(
|
||||
tmpdir,
|
||||
httpbin,
|
||||
iter([b"1234567890"]),
|
||||
iter([b"9876543210"]),
|
||||
iter([b"9876543210"]),
|
||||
)
|
||||
|
||||
# data2 and data3 should serve the same data, potentially as iterators
|
||||
def _test_body_consumed_once(
|
||||
self,
|
||||
tmpdir,
|
||||
httpbin,
|
||||
data1,
|
||||
data2,
|
||||
data3,
|
||||
):
|
||||
with NamedTemporaryFile(dir=tmpdir, suffix=".yml") as f:
|
||||
testpath = f.name
|
||||
# NOTE: ``use_cassette`` is not okay with the file existing
|
||||
# already. So we using ``.close()`` to not only
|
||||
# close but also delete the empty file, before we start.
|
||||
f.close()
|
||||
host, port = httpbin.host, httpbin.port
|
||||
match_on = ["method", "uri", "body"]
|
||||
with use_cassette(testpath, match_on=match_on):
|
||||
conn1 = httplib.HTTPConnection(host, port)
|
||||
conn1.request("POST", "/anything", body=data1)
|
||||
conn1.getresponse()
|
||||
conn2 = httplib.HTTPConnection(host, port)
|
||||
conn2.request("POST", "/anything", body=data2)
|
||||
conn2.getresponse()
|
||||
with use_cassette(testpath, match_on=match_on) as cass:
|
||||
conn3 = httplib.HTTPConnection(host, port)
|
||||
conn3.request("POST", "/anything", body=data3)
|
||||
conn3.getresponse()
|
||||
assert cass.play_counts[0] == 0
|
||||
assert cass.play_counts[1] == 1
|
||||
|
||||
33
tests/unit/test_util.py
Normal file
33
tests/unit/test_util.py
Normal file
@@ -0,0 +1,33 @@
|
||||
from io import BytesIO, StringIO
|
||||
|
||||
import pytest
|
||||
|
||||
from vcr import request
|
||||
from vcr.util import read_body
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"input_, expected_output",
|
||||
[
|
||||
(BytesIO(b"Stream"), b"Stream"),
|
||||
(StringIO("Stream"), b"Stream"),
|
||||
(iter(["StringIter"]), b"StringIter"),
|
||||
(iter(["String", "Iter"]), b"StringIter"),
|
||||
(iter([b"BytesIter"]), b"BytesIter"),
|
||||
(iter([b"Bytes", b"Iter"]), b"BytesIter"),
|
||||
(iter([70, 111, 111]), b"Foo"),
|
||||
(iter([]), b""),
|
||||
("String", b"String"),
|
||||
(b"Bytes", b"Bytes"),
|
||||
],
|
||||
)
|
||||
def test_read_body(input_, expected_output):
|
||||
r = request.Request("POST", "http://host.com/", input_, {})
|
||||
assert read_body(r) == expected_output
|
||||
|
||||
|
||||
def test_unsupported_read_body():
|
||||
r = request.Request("POST", "http://host.com/", iter([[]]), {})
|
||||
with pytest.raises(ValueError) as excinfo:
|
||||
assert read_body(r)
|
||||
assert excinfo.value.args == ("Body type <class 'list'> not supported",)
|
||||
@@ -372,3 +372,19 @@ def test_path_class_as_cassette():
|
||||
)
|
||||
with use_cassette(path):
|
||||
pass
|
||||
|
||||
|
||||
def test_use_cassette_generator_return():
|
||||
ret_val = object()
|
||||
|
||||
vcr = VCR()
|
||||
|
||||
@vcr.use_cassette("test")
|
||||
def gen():
|
||||
return ret_val
|
||||
yield
|
||||
|
||||
with pytest.raises(StopIteration) as exc_info:
|
||||
next(gen())
|
||||
|
||||
assert exc_info.value.value is ret_val
|
||||
|
||||
82
tox.ini
82
tox.ini
@@ -1,82 +0,0 @@
|
||||
[tox]
|
||||
skip_missing_interpreters=true
|
||||
envlist =
|
||||
cov-clean,
|
||||
lint,
|
||||
{py38,py39,py310,py311}-{requests-urllib3-1,httplib2,urllib3-1,tornado4,boto3,aiohttp,httpx},
|
||||
{py310,py311}-{requests-urllib3-2,urllib3-2},
|
||||
{pypy3}-{requests-urllib3-1,httplib2,urllib3-1,tornado4,boto3},
|
||||
{py310}-httpx019,
|
||||
cov-report
|
||||
|
||||
|
||||
[gh-actions]
|
||||
python =
|
||||
3.8: py38
|
||||
3.9: py39
|
||||
3.10: py310, lint
|
||||
3.11: py311
|
||||
pypy-3: pypy3
|
||||
|
||||
# Coverage environment tasks: cov-clean and cov-report
|
||||
# https://pytest-cov.readthedocs.io/en/latest/tox.html
|
||||
[testenv:cov-clean]
|
||||
deps = coverage
|
||||
skip_install=true
|
||||
commands = coverage erase
|
||||
|
||||
[testenv:cov-report]
|
||||
deps = coverage
|
||||
skip_install=true
|
||||
commands =
|
||||
coverage html
|
||||
coverage report --fail-under=90
|
||||
|
||||
[testenv:lint]
|
||||
skipsdist = True
|
||||
commands =
|
||||
black --version
|
||||
black --check --diff .
|
||||
ruff --version
|
||||
ruff check .
|
||||
deps =
|
||||
black
|
||||
ruff
|
||||
basepython = python3.10
|
||||
|
||||
[testenv]
|
||||
# Need to use develop install so that paths
|
||||
# for aggregate code coverage combine
|
||||
usedevelop=true
|
||||
commands =
|
||||
./runtests.sh --cov=./vcr --cov-branch --cov-report=xml --cov-append {posargs}
|
||||
allowlist_externals =
|
||||
./runtests.sh
|
||||
deps =
|
||||
Werkzeug==2.0.3
|
||||
pytest
|
||||
pytest-httpbin>=1.0.1
|
||||
pytest-cov
|
||||
PyYAML
|
||||
ipaddress
|
||||
requests: requests>=2.22.0
|
||||
httplib2: httplib2
|
||||
urllib3-1: urllib3<2
|
||||
urllib3-2: urllib3<3
|
||||
boto3: boto3
|
||||
aiohttp: aiohttp
|
||||
aiohttp: pytest-asyncio
|
||||
aiohttp: pytest-aiohttp
|
||||
httpx: httpx
|
||||
{py38,py39,py310}-{httpx}: httpx
|
||||
{py38,py39,py310}-{httpx}: pytest-asyncio
|
||||
httpx: httpx>0.19
|
||||
httpx019: httpx==0.19
|
||||
{py38,py39,py310}-{httpx}: pytest-asyncio
|
||||
depends =
|
||||
lint,{py38,py39,py310,py311,pypy3}-{requests-urllib3-1,httplib2,urllib3-1,tornado4,boto3},{py310,py311}-{requests-urllib3-2,urllib3-2},{py38,py39,py310,py311}-{aiohttp},{py38,py39,py310,py311}-{httpx}: cov-clean
|
||||
cov-report: lint,{py38,py39,py310,py311,pypy3}-{requests-urllib3-1,httplib2,urllib3-1,tornado4,boto3},{py310,py311}-{requests-urllib3-2,urllib3-2},{py38,py39,py310,py311}-{aiohttp}
|
||||
passenv =
|
||||
AWS_ACCESS_KEY_ID
|
||||
AWS_DEFAULT_REGION
|
||||
AWS_SECRET_ACCESS_KEY
|
||||
@@ -4,7 +4,7 @@ from logging import NullHandler
|
||||
from .config import VCR
|
||||
from .record_mode import RecordMode as mode # noqa: F401
|
||||
|
||||
__version__ = "5.1.0"
|
||||
__version__ = "6.0.2"
|
||||
|
||||
logging.getLogger(__name__).addHandler(NullHandler())
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ import contextlib
|
||||
import copy
|
||||
import inspect
|
||||
import logging
|
||||
import sys
|
||||
from asyncio import iscoroutinefunction
|
||||
|
||||
import wrapt
|
||||
@@ -126,20 +125,7 @@ class CassetteContextDecorator:
|
||||
duration of the generator.
|
||||
"""
|
||||
with self as cassette:
|
||||
coroutine = fn(cassette)
|
||||
# We don't need to catch StopIteration. The caller (Tornado's
|
||||
# gen.coroutine, for example) will handle that.
|
||||
to_yield = next(coroutine)
|
||||
while True:
|
||||
try:
|
||||
to_send = yield to_yield
|
||||
except Exception:
|
||||
to_yield = coroutine.throw(*sys.exc_info())
|
||||
else:
|
||||
try:
|
||||
to_yield = coroutine.send(to_send)
|
||||
except StopIteration:
|
||||
break
|
||||
return (yield from fn(cassette))
|
||||
|
||||
def _handle_function(self, fn):
|
||||
with self as cassette:
|
||||
|
||||
55
vcr/patch.py
55
vcr/patch.py
@@ -1,4 +1,5 @@
|
||||
"""Utilities for patching in cassettes"""
|
||||
|
||||
import contextlib
|
||||
import functools
|
||||
import http.client as httplib
|
||||
@@ -67,14 +68,6 @@ else:
|
||||
_HTTPSConnectionWithTimeout = httplib2.HTTPSConnectionWithTimeout
|
||||
_SCHEME_TO_CONNECTION = httplib2.SCHEME_TO_CONNECTION
|
||||
|
||||
# Try to save the original types for boto
|
||||
try:
|
||||
import boto.https_connection
|
||||
except ImportError: # pragma: no cover
|
||||
pass
|
||||
else:
|
||||
_CertValidatingHTTPSConnection = boto.https_connection.CertValidatingHTTPSConnection
|
||||
|
||||
# Try to save the original types for Tornado
|
||||
try:
|
||||
import tornado.simple_httpclient
|
||||
@@ -103,8 +96,8 @@ try:
|
||||
except ImportError: # pragma: no cover
|
||||
pass
|
||||
else:
|
||||
_HttpxSyncClient_send = httpx.Client.send
|
||||
_HttpxAsyncClient_send = httpx.AsyncClient.send
|
||||
_HttpxSyncClient_send_single_request = httpx.Client._send_single_request
|
||||
_HttpxAsyncClient_send_single_request = httpx.AsyncClient._send_single_request
|
||||
|
||||
|
||||
class CassettePatcherBuilder:
|
||||
@@ -126,7 +119,6 @@ class CassettePatcherBuilder:
|
||||
self._boto3(),
|
||||
self._urllib3(),
|
||||
self._httplib2(),
|
||||
self._boto(),
|
||||
self._tornado(),
|
||||
self._aiohttp(),
|
||||
self._httpx(),
|
||||
@@ -269,21 +261,14 @@ class CassettePatcherBuilder:
|
||||
|
||||
yield cpool, "HTTPConnectionWithTimeout", VCRHTTPConnectionWithTimeout
|
||||
yield cpool, "HTTPSConnectionWithTimeout", VCRHTTPSConnectionWithTimeout
|
||||
yield cpool, "SCHEME_TO_CONNECTION", {
|
||||
yield (
|
||||
cpool,
|
||||
"SCHEME_TO_CONNECTION",
|
||||
{
|
||||
"http": VCRHTTPConnectionWithTimeout,
|
||||
"https": VCRHTTPSConnectionWithTimeout,
|
||||
}
|
||||
|
||||
@_build_patchers_from_mock_triples_decorator
|
||||
def _boto(self):
|
||||
try:
|
||||
import boto.https_connection as cpool
|
||||
except ImportError: # pragma: no cover
|
||||
pass
|
||||
else:
|
||||
from .stubs.boto_stubs import VCRCertValidatingHTTPSConnection
|
||||
|
||||
yield cpool, "CertValidatingHTTPSConnection", VCRCertValidatingHTTPSConnection
|
||||
},
|
||||
)
|
||||
|
||||
@_build_patchers_from_mock_triples_decorator
|
||||
def _tornado(self):
|
||||
@@ -327,11 +312,11 @@ class CassettePatcherBuilder:
|
||||
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_async_client_send = async_vcr_send(self._cassette, _HttpxAsyncClient_send_single_request)
|
||||
yield httpx.AsyncClient, "_send_single_request", new_async_client_send
|
||||
|
||||
new_sync_client_send = sync_vcr_send(self._cassette, _HttpxSyncClient_send)
|
||||
yield httpx.Client, "send", new_sync_client_send
|
||||
new_sync_client_send = sync_vcr_send(self._cassette, _HttpxSyncClient_send_single_request)
|
||||
yield httpx.Client, "_send_single_request", new_sync_client_send
|
||||
|
||||
def _urllib3_patchers(self, cpool, conn, stubs):
|
||||
http_connection_remover = ConnectionRemover(
|
||||
@@ -388,10 +373,6 @@ class ConnectionRemover:
|
||||
if isinstance(connection, self._connection_class):
|
||||
self._connection_pool_to_connections.setdefault(pool, set()).add(connection)
|
||||
|
||||
def remove_connection_to_pool_entry(self, pool, connection):
|
||||
if isinstance(connection, self._connection_class):
|
||||
self._connection_pool_to_connections[self._connection_class].remove(connection)
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
@@ -402,10 +383,13 @@ class ConnectionRemover:
|
||||
connection = pool.pool.get()
|
||||
if isinstance(connection, self._connection_class):
|
||||
connections.remove(connection)
|
||||
connection.close()
|
||||
else:
|
||||
readd_connections.append(connection)
|
||||
for connection in readd_connections:
|
||||
pool._put_conn(connection)
|
||||
for connection in connections:
|
||||
connection.close()
|
||||
|
||||
|
||||
def reset_patchers():
|
||||
@@ -447,13 +431,6 @@ def reset_patchers():
|
||||
yield mock.patch.object(cpool, "HTTPSConnectionWithTimeout", _HTTPSConnectionWithTimeout)
|
||||
yield mock.patch.object(cpool, "SCHEME_TO_CONNECTION", _SCHEME_TO_CONNECTION)
|
||||
|
||||
try:
|
||||
import boto.https_connection as cpool
|
||||
except ImportError: # pragma: no cover
|
||||
pass
|
||||
else:
|
||||
yield mock.patch.object(cpool, "CertValidatingHTTPSConnection", _CertValidatingHTTPSConnection)
|
||||
|
||||
try:
|
||||
import tornado.simple_httpclient as simple
|
||||
except ImportError: # pragma: no cover
|
||||
|
||||
@@ -3,7 +3,7 @@ import warnings
|
||||
from io import BytesIO
|
||||
from urllib.parse import parse_qsl, urlparse
|
||||
|
||||
from .util import CaseInsensitiveDict
|
||||
from .util import CaseInsensitiveDict, _is_nonsequence_iterator
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@@ -17,8 +17,11 @@ class Request:
|
||||
self.method = method
|
||||
self.uri = uri
|
||||
self._was_file = hasattr(body, "read")
|
||||
self._was_iter = _is_nonsequence_iterator(body)
|
||||
if self._was_file:
|
||||
self.body = body.read()
|
||||
elif self._was_iter:
|
||||
self.body = list(body)
|
||||
else:
|
||||
self.body = body
|
||||
self.headers = headers
|
||||
@@ -36,7 +39,11 @@ class Request:
|
||||
|
||||
@property
|
||||
def body(self):
|
||||
return BytesIO(self._body) if self._was_file else self._body
|
||||
if self._was_file:
|
||||
return BytesIO(self._body)
|
||||
if self._was_iter:
|
||||
return iter(self._body)
|
||||
return self._body
|
||||
|
||||
@body.setter
|
||||
def body(self, value):
|
||||
|
||||
@@ -56,7 +56,7 @@ def convert_body_to_unicode(resp):
|
||||
If the request or responses body is bytes, decode it to a string
|
||||
(for python3 support)
|
||||
"""
|
||||
if type(resp) is not dict:
|
||||
if not isinstance(resp, dict):
|
||||
# Some of the tests just serialize and deserialize a string.
|
||||
return _convert_string_to_unicode(resp)
|
||||
else:
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
try:
|
||||
import simplejson as json
|
||||
except ImportError:
|
||||
import json
|
||||
|
||||
|
||||
|
||||
@@ -389,6 +389,8 @@ class VCRHTTPConnection(VCRConnection):
|
||||
|
||||
_baseclass = HTTPConnection
|
||||
_protocol = "http"
|
||||
debuglevel = _baseclass.debuglevel
|
||||
_http_vsn = _baseclass._http_vsn
|
||||
|
||||
|
||||
class VCRHTTPSConnection(VCRConnection):
|
||||
@@ -397,3 +399,5 @@ class VCRHTTPSConnection(VCRConnection):
|
||||
_baseclass = HTTPSConnection
|
||||
_protocol = "https"
|
||||
is_verified = True
|
||||
debuglevel = _baseclass.debuglevel
|
||||
_http_vsn = _baseclass._http_vsn
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
"""Stubs for aiohttp HTTP clients"""
|
||||
|
||||
import asyncio
|
||||
import functools
|
||||
import json
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
"""Stubs for boto3"""
|
||||
|
||||
from botocore.awsrequest import AWSHTTPConnection as HTTPConnection
|
||||
from botocore.awsrequest import AWSHTTPSConnection as VerifiedHTTPSConnection
|
||||
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
"""Stubs for boto"""
|
||||
|
||||
from boto.https_connection import CertValidatingHTTPSConnection
|
||||
|
||||
from ..stubs import VCRHTTPSConnection
|
||||
|
||||
|
||||
class VCRCertValidatingHTTPSConnection(VCRHTTPSConnection):
|
||||
_baseclass = CertValidatingHTTPSConnection
|
||||
@@ -1,3 +1,4 @@
|
||||
import asyncio
|
||||
import functools
|
||||
import inspect
|
||||
import logging
|
||||
@@ -6,7 +7,9 @@ from unittest.mock import MagicMock, patch
|
||||
import httpx
|
||||
|
||||
from vcr.errors import CannotOverwriteExistingCassetteException
|
||||
from vcr.filters import decode_response
|
||||
from vcr.request import Request as VcrRequest
|
||||
from vcr.serializers.compat import convert_body_to_bytes
|
||||
|
||||
_httpx_signature = inspect.signature(httpx.Client.request)
|
||||
|
||||
@@ -33,14 +36,29 @@ def _transform_headers(httpx_response):
|
||||
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"),
|
||||
async def _to_serialized_response(resp, aread):
|
||||
# The content shouldn't already have been read in by HTTPX.
|
||||
assert not hasattr(resp, "_decoder")
|
||||
|
||||
# Retrieve the content, but without decoding it.
|
||||
with patch.dict(resp.headers, {"Content-Encoding": ""}):
|
||||
if aread:
|
||||
await resp.aread()
|
||||
else:
|
||||
resp.read()
|
||||
|
||||
result = {
|
||||
"status": {"code": resp.status_code, "message": resp.reason_phrase},
|
||||
"headers": _transform_headers(resp),
|
||||
"body": {"string": resp.content},
|
||||
}
|
||||
|
||||
# As the content wasn't decoded, we restore the response to a state which
|
||||
# will be capable of decoding the content for the consumer.
|
||||
del resp._decoder
|
||||
resp._content = resp._get_content_decoder().decode(resp.content)
|
||||
return result
|
||||
|
||||
|
||||
def _from_serialized_headers(headers):
|
||||
"""
|
||||
@@ -57,15 +75,32 @@ def _from_serialized_headers(headers):
|
||||
@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 [],
|
||||
# Cassette format generated for HTTPX requests by older versions of
|
||||
# vcrpy. We restructure the content to resemble what a regular
|
||||
# cassette looks like.
|
||||
if "status_code" in serialized_response:
|
||||
serialized_response = decode_response(
|
||||
convert_body_to_bytes(
|
||||
{
|
||||
"headers": serialized_response["headers"],
|
||||
"body": {"string": serialized_response["content"]},
|
||||
"status": {"code": serialized_response["status_code"]},
|
||||
},
|
||||
),
|
||||
)
|
||||
response._content = content
|
||||
extensions = None
|
||||
else:
|
||||
extensions = {"reason_phrase": serialized_response["status"]["message"].encode()}
|
||||
|
||||
response = httpx.Response(
|
||||
status_code=serialized_response["status"]["code"],
|
||||
request=request,
|
||||
headers=_from_serialized_headers(serialized_response["headers"]),
|
||||
content=serialized_response["body"]["string"],
|
||||
history=history or [],
|
||||
extensions=extensions,
|
||||
)
|
||||
|
||||
return response
|
||||
|
||||
|
||||
@@ -91,45 +126,23 @@ def _shared_vcr_send(cassette, real_send, *args, **kwargs):
|
||||
return vcr_request, None
|
||||
|
||||
|
||||
def _record_responses(cassette, vcr_request, real_response):
|
||||
async def _record_responses(cassette, vcr_request, real_response, aread):
|
||||
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))
|
||||
cassette.append(past_vcr_request, await _to_serialized_response(past_real_response, aread))
|
||||
|
||||
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))
|
||||
cassette.append(vcr_request, await _to_serialized_response(real_response, aread))
|
||||
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
|
||||
|
||||
|
||||
@@ -141,7 +154,8 @@ async def _async_vcr_send(cassette, real_send, *args, **kwargs):
|
||||
return response
|
||||
|
||||
real_response = await real_send(*args, **kwargs)
|
||||
return _record_responses(cassette, vcr_request, real_response)
|
||||
await _record_responses(cassette, vcr_request, real_response, aread=True)
|
||||
return real_response
|
||||
|
||||
|
||||
def async_vcr_send(cassette, real_send):
|
||||
@@ -160,7 +174,8 @@ def _sync_vcr_send(cassette, real_send, *args, **kwargs):
|
||||
return response
|
||||
|
||||
real_response = real_send(*args, **kwargs)
|
||||
return _record_responses(cassette, vcr_request, real_response)
|
||||
asyncio.run(_record_responses(cassette, vcr_request, real_response, aread=False))
|
||||
return real_response
|
||||
|
||||
|
||||
def sync_vcr_send(cassette, real_send):
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
"""Stubs for tornado HTTP clients"""
|
||||
|
||||
import functools
|
||||
from io import BytesIO
|
||||
|
||||
|
||||
19
vcr/util.py
19
vcr/util.py
@@ -89,9 +89,28 @@ def compose(*functions):
|
||||
return composed
|
||||
|
||||
|
||||
def _is_nonsequence_iterator(obj):
|
||||
return hasattr(obj, "__iter__") and not isinstance(
|
||||
obj,
|
||||
(bytearray, bytes, dict, list, str),
|
||||
)
|
||||
|
||||
|
||||
def read_body(request):
|
||||
if hasattr(request.body, "read"):
|
||||
return request.body.read()
|
||||
if _is_nonsequence_iterator(request.body):
|
||||
body = list(request.body)
|
||||
if body:
|
||||
if isinstance(body[0], str):
|
||||
return "".join(body).encode("utf-8")
|
||||
elif isinstance(body[0], (bytes, bytearray)):
|
||||
return b"".join(body)
|
||||
elif isinstance(body[0], int):
|
||||
return bytes(body)
|
||||
else:
|
||||
raise ValueError(f"Body type {type(body[0])} not supported")
|
||||
return b""
|
||||
return request.body
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user