mirror of
https://github.com/kevin1024/vcrpy.git
synced 2025-12-09 01:03:24 +00:00
Compare commits
70 Commits
bump-werkz
...
v6.0.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1e3a5ac753 | ||
|
|
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 | ||
|
|
f4144359f6 |
4
.github/workflows/docs.yml
vendored
4
.github/workflows/docs.yml
vendored
@@ -11,10 +11,10 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: actions/setup-python@v4
|
- uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
python-version: "3.12"
|
python-version: "3.12"
|
||||||
|
|
||||||
- name: Install build dependencies
|
- name: Install build dependencies
|
||||||
run: pip install -r docs/requirements.txt
|
run: pip install -r docs/requirements.txt
|
||||||
- name: Rendering HTML documentation
|
- name: Rendering HTML documentation
|
||||||
|
|||||||
43
.github/workflows/main.yml
vendored
43
.github/workflows/main.yml
vendored
@@ -13,31 +13,56 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "pypy-3.8", "pypy-3.9", "pypy-3.10"]
|
python-version:
|
||||||
|
- "3.8"
|
||||||
|
- "3.9"
|
||||||
|
- "3.10"
|
||||||
|
- "3.11"
|
||||||
|
- "3.12"
|
||||||
|
- "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:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Set up Python ${{ matrix.python-version }}
|
- name: Set up Python ${{ matrix.python-version }}
|
||||||
uses: actions/setup-python@v4
|
uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.python-version }}
|
python-version: ${{ matrix.python-version }}
|
||||||
|
cache: pip
|
||||||
|
|
||||||
- name: Install project dependencies
|
- name: Install project dependencies
|
||||||
run: |
|
run: |
|
||||||
pip3 install --upgrade pip
|
pip install --upgrade pip
|
||||||
pip3 install codecov tox tox-gh-actions
|
pip install codecov '.[tests]' '${{ matrix.urllib3-requirement }}'
|
||||||
|
pip check
|
||||||
|
|
||||||
- name: Run online tests with tox
|
- name: Run online tests
|
||||||
run: tox -- -m online
|
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: |
|
run: |
|
||||||
# We're using unshare to take Internet access
|
# 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
|
# is missing @pytest.mark.online decoration in the future
|
||||||
unshare --map-root-user --net -- \
|
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
|
- name: Run coverage
|
||||||
run: codecov
|
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@v5
|
||||||
|
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.0
|
||||||
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.1.13
|
||||||
|
hooks:
|
||||||
|
- id: ruff
|
||||||
|
args: ["--show-source"]
|
||||||
|
- id: ruff-format
|
||||||
|
|
||||||
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||||
|
rev: v4.5.0
|
||||||
|
hooks:
|
||||||
|
- id: check-merge-conflict
|
||||||
|
- id: end-of-file-fixer
|
||||||
|
- id: trailing-whitespace
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
include README.rst
|
include README.rst
|
||||||
include LICENSE.txt
|
include LICENSE.txt
|
||||||
include tox.ini
|
|
||||||
recursive-include tests *
|
recursive-include tests *
|
||||||
recursive-exclude * __pycache__
|
recursive-exclude * __pycache__
|
||||||
recursive-exclude * *.py[co]
|
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
|
.. |CodeCov| image:: https://codecov.io/gh/kevin1024/vcrpy/branch/master/graph/badge.svg
|
||||||
:target: https://codecov.io/gh/kevin1024/vcrpy
|
:target: https://codecov.io/gh/kevin1024/vcrpy
|
||||||
:alt: Code Coverage Status
|
: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
|
|
||||||
|
|||||||
2
docs/_static/vcr.svg
vendored
2
docs/_static/vcr.svg
vendored
@@ -24,4 +24,4 @@
|
|||||||
<stop offset="1" stop-color="#27DDA6"/>
|
<stop offset="1" stop-color="#27DDA6"/>
|
||||||
</linearGradient>
|
</linearGradient>
|
||||||
</defs>
|
</defs>
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 6.2 KiB After Width: | Height: | Size: 6.2 KiB |
@@ -16,7 +16,7 @@ a nice addition. Here's an example:
|
|||||||
with vcr.use_cassette('fixtures/vcr_cassettes/synopsis.yaml') as cass:
|
with vcr.use_cassette('fixtures/vcr_cassettes/synopsis.yaml') as cass:
|
||||||
response = urllib2.urlopen('http://www.zombo.com/').read()
|
response = urllib2.urlopen('http://www.zombo.com/').read()
|
||||||
# cass should have 1 request inside it
|
# cass should have 1 request inside it
|
||||||
assert len(cass) == 1
|
assert len(cass) == 1
|
||||||
# the request uri should have been http://www.zombo.com/
|
# the request uri should have been http://www.zombo.com/
|
||||||
assert cass.requests[0].uri == 'http://www.zombo.com/'
|
assert cass.requests[0].uri == 'http://www.zombo.com/'
|
||||||
|
|
||||||
@@ -208,7 +208,7 @@ So these two calls are the same:
|
|||||||
|
|
||||||
# original (still works)
|
# original (still works)
|
||||||
vcr = VCR(filter_headers=['authorization'])
|
vcr = VCR(filter_headers=['authorization'])
|
||||||
|
|
||||||
# new
|
# new
|
||||||
vcr = VCR(filter_headers=[('authorization', None)])
|
vcr = VCR(filter_headers=[('authorization', None)])
|
||||||
|
|
||||||
@@ -218,7 +218,7 @@ Here are two examples of the new functionality:
|
|||||||
|
|
||||||
# replace with a static value (most common)
|
# replace with a static value (most common)
|
||||||
vcr = VCR(filter_headers=[('authorization', 'XXXXXX')])
|
vcr = VCR(filter_headers=[('authorization', 'XXXXXX')])
|
||||||
|
|
||||||
# replace with a callable, for example when testing
|
# replace with a callable, for example when testing
|
||||||
# lots of different kinds of authorization.
|
# lots of different kinds of authorization.
|
||||||
def replace_auth(key, value, request):
|
def replace_auth(key, value, request):
|
||||||
@@ -286,7 +286,7 @@ sensitive data from the response body:
|
|||||||
before_record_response=scrub_string(settings.USERNAME, 'username'),
|
before_record_response=scrub_string(settings.USERNAME, 'username'),
|
||||||
)
|
)
|
||||||
with my_vcr.use_cassette('test.yml'):
|
with my_vcr.use_cassette('test.yml'):
|
||||||
# your http code here
|
# your http code here
|
||||||
|
|
||||||
|
|
||||||
Decode compressed response
|
Decode compressed response
|
||||||
|
|||||||
@@ -7,6 +7,14 @@ 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.
|
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.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
|
- 5.1.0
|
||||||
- Use ruff for linting (instead of current flake8/isort/pyflakes) - thanks @jairhenrique
|
- Use ruff for linting (instead of current flake8/isort/pyflakes) - thanks @jairhenrique
|
||||||
- Enable rule B (flake8-bugbear) on ruff - thanks @jairhenrique
|
- Enable rule B (flake8-bugbear) on ruff - thanks @jairhenrique
|
||||||
@@ -287,4 +295,3 @@ All help in providing PRs to close out bug issues is appreciated. Even if that i
|
|||||||
- Add support for requests / urllib3
|
- Add support for requests / urllib3
|
||||||
- 0.0.1
|
- 0.0.1
|
||||||
- Initial Release
|
- Initial Release
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ So whilst reporting issues are valuable, please consider:
|
|||||||
- contributing an issue with a toy repo that replicates the issue.
|
- contributing an issue with a toy repo that replicates the issue.
|
||||||
- contributing PRs is a more valuable donation of your time and effort.
|
- contributing PRs is a more valuable donation of your time and effort.
|
||||||
|
|
||||||
Thanks again for your interest and support in VCRpy.
|
Thanks again for your interest and support in VCRpy.
|
||||||
|
|
||||||
We really appreciate it.
|
We really appreciate it.
|
||||||
|
|
||||||
@@ -57,7 +57,7 @@ Simply adding these three labels for incoming issues means a lot for maintaining
|
|||||||
- Which library does it affect? ``core``, ``aiohttp``, ``requests``, ``urllib3``, ``tornado4``, ``httplib2``
|
- Which library does it affect? ``core``, ``aiohttp``, ``requests``, ``urllib3``, ``tornado4``, ``httplib2``
|
||||||
- If it is a bug, is it ``Verified Can Replicate`` or ``Requires Help Replicating``
|
- If it is a bug, is it ``Verified Can Replicate`` or ``Requires Help Replicating``
|
||||||
- Thanking people for raising issues. Feedback is always appreciated.
|
- Thanking people for raising issues. Feedback is always appreciated.
|
||||||
- Politely asking if they are able to link to an example repo that replicates the issue if they haven't already. Being able to *clone and go* helps the next person and we like that. 😃
|
- Politely asking if they are able to link to an example repo that replicates the issue if they haven't already. Being able to *clone and go* helps the next person and we like that. 😃
|
||||||
|
|
||||||
**Maintainer:**
|
**Maintainer:**
|
||||||
|
|
||||||
@@ -68,7 +68,7 @@ This involves creating PRs to address bugs and enhancement requests. It also mea
|
|||||||
The PR reviewer is a second set of eyes to see if:
|
The PR reviewer is a second set of eyes to see if:
|
||||||
- Are there tests covering the code paths added/modified?
|
- Are there tests covering the code paths added/modified?
|
||||||
- Do the tests and modifications make sense seem appropriate?
|
- Do the tests and modifications make sense seem appropriate?
|
||||||
- Add specific feedback, even on approvals, why it is accepted. eg "I like how you use a context manager there. 😄 "
|
- Add specific feedback, even on approvals, why it is accepted. eg "I like how you use a context manager there. 😄 "
|
||||||
- Also make sure they add a line to `docs/changelog.rst` to claim credit for their contribution.
|
- Also make sure they add a line to `docs/changelog.rst` to claim credit for their contribution.
|
||||||
|
|
||||||
**Release Manager:**
|
**Release Manager:**
|
||||||
@@ -83,39 +83,21 @@ The PR reviewer is a second set of eyes to see if:
|
|||||||
Running VCR's test suite
|
Running VCR's test suite
|
||||||
------------------------
|
------------------------
|
||||||
|
|
||||||
The tests are all run automatically on `Travis
|
The tests are all run automatically on `Github Actions CI <https://github.com/kevin1024/vcrpy/actions>`__,
|
||||||
CI <https://travis-ci.org/kevin1024/vcrpy>`__, but you can also run them
|
but you can also run them yourself using `pytest <http://pytest.org/>`__.
|
||||||
yourself using `pytest <http://pytest.org/>`__ and
|
|
||||||
`Tox <http://tox.testrun.org/>`__.
|
|
||||||
|
|
||||||
Tox will automatically run them in all environments VCR.py supports if they are available on your `PATH`. Alternatively you can use `tox-pyenv <https://pypi.org/project/tox-pyenv/>`_ with
|
In order for the boto3 tests to run, you will need an AWS key.
|
||||||
`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 boto3 tests to run, you will need an AWS key.
|
|
||||||
Refer to the `boto3
|
Refer to the `boto3
|
||||||
documentation <https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/index.html>`__
|
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
|
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
|
Travis so you don't have to worry about them failing if you submit a
|
||||||
pull request.
|
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>`_
|
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::
|
in this example::
|
||||||
|
|
||||||
git clone https://github.com/pyenv/pyenv ~/.pyenv
|
git clone https://github.com/pyenv/pyenv ~/.pyenv
|
||||||
@@ -126,27 +108,21 @@ in this example::
|
|||||||
# Setup shim paths
|
# Setup shim paths
|
||||||
eval "$(pyenv init -)"
|
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
|
# 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
|
# This activates them
|
||||||
pyenv local 3.8.0 pypy3.8
|
pyenv local 3.12.0 pypy3.10
|
||||||
|
|
||||||
# Run the whole test suite
|
# Run the whole test suite
|
||||||
tox
|
pip install .[test]
|
||||||
|
./run_tests.sh
|
||||||
# Run the whole test suite or just part of it
|
|
||||||
tox -e lint
|
|
||||||
tox -e py38-requests
|
|
||||||
|
|
||||||
|
|
||||||
Troubleshooting on MacOSX
|
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
|
.. code:: python
|
||||||
|
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
sphinx<8
|
sphinx<8
|
||||||
sphinx_rtd_theme==1.3.0
|
sphinx_rtd_theme==2.0.0
|
||||||
|
|||||||
@@ -1,6 +1,3 @@
|
|||||||
[tool.black]
|
|
||||||
line-length=110
|
|
||||||
|
|
||||||
[tool.codespell]
|
[tool.codespell]
|
||||||
skip = '.git,*.pdf,*.svg,.tox'
|
skip = '.git,*.pdf,*.svg,.tox'
|
||||||
ignore-regex = "\\\\[fnrstv]"
|
ignore-regex = "\\\\[fnrstv]"
|
||||||
@@ -8,8 +5,14 @@ ignore-regex = "\\\\[fnrstv]"
|
|||||||
# ignore-words-list = ''
|
# ignore-words-list = ''
|
||||||
|
|
||||||
[tool.pytest.ini_options]
|
[tool.pytest.ini_options]
|
||||||
markers = [
|
addopts = [
|
||||||
"online",
|
"--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]
|
[tool.ruff]
|
||||||
@@ -30,4 +33,4 @@ line-length = 110
|
|||||||
target-version = "py38"
|
target-version = "py38"
|
||||||
|
|
||||||
[tool.ruff.isort]
|
[tool.ruff.isort]
|
||||||
known-first-party = [ "vcr" ]
|
known-first-party = ["vcr"]
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
#!/bin/bash
|
#!/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.
|
||||||
# If you are getting an INVOCATION ERROR for this script then there is
|
# You can and should use WSL for running tests on Windows when it calls bash scripts.
|
||||||
# a good chance you are running on Windows.
|
|
||||||
# You can and should use WSL for running tox on Windows when it calls bash scripts.
|
|
||||||
REQUESTS_CA_BUNDLE=`python3 -m pytest_httpbin.certs` exec pytest "$@"
|
REQUESTS_CA_BUNDLE=`python3 -m pytest_httpbin.certs` exec pytest "$@"
|
||||||
|
|||||||
44
setup.py
44
setup.py
@@ -57,24 +57,29 @@ install_requires = [
|
|||||||
"urllib3 <2; platform_python_implementation =='PyPy'",
|
"urllib3 <2; platform_python_implementation =='PyPy'",
|
||||||
]
|
]
|
||||||
|
|
||||||
tests_require = [
|
extras_require = {
|
||||||
"aiohttp",
|
"tests": [
|
||||||
"boto3",
|
"aiohttp",
|
||||||
"httplib2",
|
"boto3",
|
||||||
"httpx",
|
"httplib2",
|
||||||
"pytest",
|
"httpx",
|
||||||
"pytest-aiohttp",
|
"pytest-aiohttp",
|
||||||
"pytest-httpbin",
|
"pytest-asyncio",
|
||||||
"requests>=2.16.2",
|
"pytest-cov",
|
||||||
"tornado",
|
"pytest-httpbin",
|
||||||
# Needed to un-break httpbin 0.7.0. For httpbin >=0.7.1 and after,
|
"pytest",
|
||||||
# this pin and the dependency itself can be removed, provided
|
"requests>=2.22.0",
|
||||||
# that the related bug in httpbin has been fixed:
|
"tornado",
|
||||||
# https://github.com/kevin1024/vcrpy/issues/645#issuecomment-1562489489
|
"urllib3",
|
||||||
# https://github.com/postmanlabs/httpbin/issues/673
|
# Needed to un-break httpbin 0.7.0. For httpbin >=0.7.1 and after,
|
||||||
# https://github.com/postmanlabs/httpbin/pull/674
|
# this pin and the dependency itself can be removed, provided
|
||||||
"Werkzeug==2.0.3",
|
# that the related bug in httpbin has been fixed:
|
||||||
]
|
# https://github.com/kevin1024/vcrpy/issues/645#issuecomment-1562489489
|
||||||
|
# https://github.com/postmanlabs/httpbin/issues/673
|
||||||
|
# https://github.com/postmanlabs/httpbin/pull/674
|
||||||
|
"Werkzeug==2.0.3",
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
setup(
|
setup(
|
||||||
name="vcrpy",
|
name="vcrpy",
|
||||||
@@ -89,7 +94,8 @@ setup(
|
|||||||
python_requires=">=3.8",
|
python_requires=">=3.8",
|
||||||
install_requires=install_requires,
|
install_requires=install_requires,
|
||||||
license="MIT",
|
license="MIT",
|
||||||
tests_require=tests_require,
|
extras_require=extras_require,
|
||||||
|
tests_require=extras_require["tests"],
|
||||||
classifiers=[
|
classifiers=[
|
||||||
"Development Status :: 5 - Production/Stable",
|
"Development Status :: 5 - Production/Stable",
|
||||||
"Environment :: Console",
|
"Environment :: Console",
|
||||||
|
|||||||
0
tests/__init__.py
Normal file
0
tests/__init__.py
Normal file
6
tests/fixtures/migration/new_cassette.json
vendored
6
tests/fixtures/migration/new_cassette.json
vendored
@@ -15,9 +15,9 @@
|
|||||||
},
|
},
|
||||||
"response": {
|
"response": {
|
||||||
"status": {
|
"status": {
|
||||||
"message": "OK",
|
"message": "OK",
|
||||||
"code": 200
|
"code": 200
|
||||||
},
|
},
|
||||||
"headers": {
|
"headers": {
|
||||||
"access-control-allow-origin": ["*"],
|
"access-control-allow-origin": ["*"],
|
||||||
"content-type": ["application/json"],
|
"content-type": ["application/json"],
|
||||||
@@ -25,7 +25,7 @@
|
|||||||
"server": ["gunicorn/0.17.4"],
|
"server": ["gunicorn/0.17.4"],
|
||||||
"content-length": ["32"],
|
"content-length": ["32"],
|
||||||
"connection": ["keep-alive"]
|
"connection": ["keep-alive"]
|
||||||
},
|
},
|
||||||
"body": {
|
"body": {
|
||||||
"string": "{\n \"origin\": \"217.122.164.194\"\n}"
|
"string": "{\n \"origin\": \"217.122.164.194\"\n}"
|
||||||
}
|
}
|
||||||
|
|||||||
2
tests/fixtures/migration/new_cassette.yaml
vendored
2
tests/fixtures/migration/new_cassette.yaml
vendored
@@ -2,7 +2,7 @@ version: 1
|
|||||||
interactions:
|
interactions:
|
||||||
- request:
|
- request:
|
||||||
body: null
|
body: null
|
||||||
headers:
|
headers:
|
||||||
accept: ['*/*']
|
accept: ['*/*']
|
||||||
accept-encoding: ['gzip, deflate, compress']
|
accept-encoding: ['gzip, deflate, compress']
|
||||||
user-agent: ['python-requests/2.2.1 CPython/2.6.1 Darwin/10.8.0']
|
user-agent: ['python-requests/2.2.1 CPython/2.6.1 Darwin/10.8.0']
|
||||||
|
|||||||
34
tests/fixtures/migration/old_cassette.json
vendored
34
tests/fixtures/migration/old_cassette.json
vendored
@@ -1,31 +1,31 @@
|
|||||||
[
|
[
|
||||||
{
|
{
|
||||||
"request": {
|
"request": {
|
||||||
"body": null,
|
"body": null,
|
||||||
"protocol": "http",
|
"protocol": "http",
|
||||||
"method": "GET",
|
"method": "GET",
|
||||||
"headers": {
|
"headers": {
|
||||||
"accept-encoding": "gzip, deflate, compress",
|
"accept-encoding": "gzip, deflate, compress",
|
||||||
"accept": "*/*",
|
"accept": "*/*",
|
||||||
"user-agent": "python-requests/2.2.1 CPython/2.6.1 Darwin/10.8.0"
|
"user-agent": "python-requests/2.2.1 CPython/2.6.1 Darwin/10.8.0"
|
||||||
},
|
},
|
||||||
"host": "httpbin.org",
|
"host": "httpbin.org",
|
||||||
"path": "/ip",
|
"path": "/ip",
|
||||||
"port": 80
|
"port": 80
|
||||||
},
|
},
|
||||||
"response": {
|
"response": {
|
||||||
"status": {
|
"status": {
|
||||||
"message": "OK",
|
"message": "OK",
|
||||||
"code": 200
|
"code": 200
|
||||||
},
|
},
|
||||||
"headers": [
|
"headers": [
|
||||||
"access-control-allow-origin: *\r\n",
|
"access-control-allow-origin: *\r\n",
|
||||||
"content-type: application/json\r\n",
|
"content-type: application/json\r\n",
|
||||||
"date: Mon, 21 Apr 2014 23:13:40 GMT\r\n",
|
"date: Mon, 21 Apr 2014 23:13:40 GMT\r\n",
|
||||||
"server: gunicorn/0.17.4\r\n",
|
"server: gunicorn/0.17.4\r\n",
|
||||||
"content-length: 32\r\n",
|
"content-length: 32\r\n",
|
||||||
"connection: keep-alive\r\n"
|
"connection: keep-alive\r\n"
|
||||||
],
|
],
|
||||||
"body": {
|
"body": {
|
||||||
"string": "{\n \"origin\": \"217.122.164.194\"\n}"
|
"string": "{\n \"origin\": \"217.122.164.194\"\n}"
|
||||||
}
|
}
|
||||||
|
|||||||
2
tests/fixtures/wild/domain_redirect.yaml
vendored
2
tests/fixtures/wild/domain_redirect.yaml
vendored
@@ -10,7 +10,7 @@ interactions:
|
|||||||
uri: http://seomoz.org/
|
uri: http://seomoz.org/
|
||||||
response:
|
response:
|
||||||
body: {string: ''}
|
body: {string: ''}
|
||||||
headers:
|
headers:
|
||||||
Location: ['http://moz.com/']
|
Location: ['http://moz.com/']
|
||||||
Server: ['BigIP']
|
Server: ['BigIP']
|
||||||
Connection: ['Keep-Alive']
|
Connection: ['Keep-Alive']
|
||||||
|
|||||||
@@ -5,24 +5,24 @@ import aiohttp
|
|||||||
|
|
||||||
|
|
||||||
async def aiohttp_request(loop, method, url, output="text", encoding="utf-8", content_type=None, **kwargs):
|
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_ctx = session.request(method, url, **kwargs)
|
||||||
|
|
||||||
response = await response_ctx.__aenter__()
|
response = await response_ctx.__aenter__()
|
||||||
if output == "text":
|
if output == "text":
|
||||||
content = await response.text()
|
content = await response.text()
|
||||||
elif output == "json":
|
elif output == "json":
|
||||||
content_type = content_type or "application/json"
|
content_type = content_type or "application/json"
|
||||||
content = await response.json(encoding=encoding, content_type=content_type)
|
content = await response.json(encoding=encoding, content_type=content_type)
|
||||||
elif output == "raw":
|
elif output == "raw":
|
||||||
content = await response.read()
|
content = await response.read()
|
||||||
elif output == "stream":
|
elif output == "stream":
|
||||||
content = await response.content.read()
|
content = await response.content.read()
|
||||||
|
|
||||||
response_ctx._resp.close()
|
response_ctx._resp.close()
|
||||||
await session.close()
|
await session.close()
|
||||||
|
|
||||||
return response, content
|
return response, content
|
||||||
|
|
||||||
|
|
||||||
def aiohttp_app():
|
def aiohttp_app():
|
||||||
|
|||||||
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,4 +1,3 @@
|
|||||||
import contextlib
|
|
||||||
import logging
|
import logging
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
|
|
||||||
@@ -14,10 +13,10 @@ from .aiohttp_utils import aiohttp_app, aiohttp_request # noqa: E402
|
|||||||
|
|
||||||
|
|
||||||
def run_in_loop(fn):
|
def run_in_loop(fn):
|
||||||
with contextlib.closing(asyncio.new_event_loop()) as loop:
|
async def wrapper():
|
||||||
asyncio.set_event_loop(loop)
|
return await fn(asyncio.get_running_loop())
|
||||||
task = loop.create_task(fn(loop))
|
|
||||||
return loop.run_until_complete(task)
|
return asyncio.run(wrapper())
|
||||||
|
|
||||||
|
|
||||||
def request(method, url, output="text", **kwargs):
|
def request(method, url, output="text", **kwargs):
|
||||||
@@ -260,6 +259,12 @@ def test_aiohttp_test_client_json(aiohttp_client, tmpdir):
|
|||||||
assert cassette.play_count == 1
|
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
|
@pytest.mark.online
|
||||||
def test_redirect(tmpdir, httpbin):
|
def test_redirect(tmpdir, httpbin):
|
||||||
url = httpbin.url + "/redirect/2"
|
url = httpbin.url + "/redirect/2"
|
||||||
|
|||||||
@@ -5,10 +5,11 @@ from urllib.parse import urlencode
|
|||||||
from urllib.request import Request, urlopen
|
from urllib.request import Request, urlopen
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from assertions import assert_cassette_has_one_response, assert_is_json_bytes
|
|
||||||
|
|
||||||
import vcr
|
import vcr
|
||||||
|
|
||||||
|
from ..assertions import assert_cassette_has_one_response, assert_is_json_bytes
|
||||||
|
|
||||||
|
|
||||||
def _request_with_auth(url, username, password):
|
def _request_with_auth(url, username, password):
|
||||||
request = Request(url)
|
request = Request(url)
|
||||||
|
|||||||
@@ -3,10 +3,11 @@ from urllib.parse import urlencode
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
import pytest_httpbin.certs
|
import pytest_httpbin.certs
|
||||||
from assertions import assert_cassette_has_one_response
|
|
||||||
|
|
||||||
import vcr
|
import vcr
|
||||||
|
|
||||||
|
from ..assertions import assert_cassette_has_one_response
|
||||||
|
|
||||||
httplib2 = pytest.importorskip("httplib2")
|
httplib2 = pytest.importorskip("httplib2")
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
|
import os
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
import vcr
|
import vcr
|
||||||
|
|
||||||
|
from ..assertions import assert_is_json_bytes
|
||||||
|
|
||||||
asyncio = pytest.importorskip("asyncio")
|
asyncio = pytest.importorskip("asyncio")
|
||||||
httpx = pytest.importorskip("httpx")
|
httpx = pytest.importorskip("httpx")
|
||||||
|
|
||||||
@@ -28,25 +32,37 @@ class DoSyncRequest(BaseDoRequest):
|
|||||||
_client_class = httpx.Client
|
_client_class = httpx.Client
|
||||||
|
|
||||||
def __enter__(self):
|
def __enter__(self):
|
||||||
|
self._client = self._make_client()
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def __exit__(self, *args):
|
def __exit__(self, *args):
|
||||||
pass
|
self._client.close()
|
||||||
|
del self._client
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def client(self):
|
def client(self):
|
||||||
try:
|
try:
|
||||||
return self._client
|
return self._client
|
||||||
except AttributeError:
|
except AttributeError as e:
|
||||||
self._client = self._make_client()
|
raise ValueError('To access sync client, use "with do_request() as client"') from e
|
||||||
return self._client
|
|
||||||
|
|
||||||
def __call__(self, *args, **kwargs):
|
def __call__(self, *args, **kwargs):
|
||||||
return self.client.request(*args, timeout=60, **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):
|
def stream(self, *args, **kwargs):
|
||||||
with self.client.stream(*args, **kwargs) as response:
|
if hasattr(self, "_client"):
|
||||||
return b"".join(response.iter_bytes())
|
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):
|
class DoAsyncRequest(BaseDoRequest):
|
||||||
@@ -207,22 +223,6 @@ def test_redirect(httpbin, yml, do_request):
|
|||||||
assert cassette_response.request.headers.items() == response.request.headers.items()
|
assert cassette_response.request.headers.items() == response.request.headers.items()
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.online
|
|
||||||
def test_work_with_gzipped_data(httpbin, do_request, yml):
|
|
||||||
url = httpbin.url + "/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.online
|
||||||
@pytest.mark.parametrize("url", ["https://github.com/kevin1024/vcrpy/issues/" + str(i) for i in range(3, 6)])
|
@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):
|
def test_simple_fetching(do_request, yml, url):
|
||||||
@@ -285,3 +285,77 @@ def test_stream(tmpdir, httpbin, do_request):
|
|||||||
assert cassette_content == response_content
|
assert cassette_content == response_content
|
||||||
assert len(cassette_content) == 512
|
assert len(cassette_content) == 512
|
||||||
assert cassette.play_count == 1
|
assert cassette.play_count == 1
|
||||||
|
|
||||||
|
|
||||||
|
# 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:
|
||||||
|
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
|
||||||
|
|||||||
@@ -39,12 +39,12 @@ class Proxy(http.server.SimpleHTTPRequestHandler):
|
|||||||
|
|
||||||
@pytest.fixture(scope="session")
|
@pytest.fixture(scope="session")
|
||||||
def proxy_server():
|
def proxy_server():
|
||||||
httpd = socketserver.ThreadingTCPServer(("", 0), Proxy)
|
with socketserver.ThreadingTCPServer(("", 0), Proxy) as httpd:
|
||||||
proxy_process = threading.Thread(target=httpd.serve_forever)
|
proxy_process = threading.Thread(target=httpd.serve_forever)
|
||||||
proxy_process.start()
|
proxy_process.start()
|
||||||
yield "http://{}:{}".format(*httpd.server_address)
|
yield "http://{}:{}".format(*httpd.server_address)
|
||||||
httpd.shutdown()
|
httpd.shutdown()
|
||||||
proxy_process.join()
|
proxy_process.join()
|
||||||
|
|
||||||
|
|
||||||
def test_use_proxy(tmpdir, httpbin, proxy_server):
|
def test_use_proxy(tmpdir, httpbin, proxy_server):
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
"""Test requests' interaction with vcr"""
|
"""Test requests' interaction with vcr"""
|
||||||
import pytest
|
import pytest
|
||||||
from assertions import assert_cassette_empty, assert_is_json_bytes
|
|
||||||
|
|
||||||
import vcr
|
import vcr
|
||||||
|
|
||||||
|
from ..assertions import assert_cassette_empty, assert_is_json_bytes
|
||||||
|
|
||||||
requests = pytest.importorskip("requests")
|
requests = pytest.importorskip("requests")
|
||||||
|
|
||||||
|
|
||||||
@@ -264,7 +265,7 @@ def test_nested_cassettes_with_session_created_before_nesting(httpbin_both, tmpd
|
|||||||
def test_post_file(tmpdir, httpbin_both):
|
def test_post_file(tmpdir, httpbin_both):
|
||||||
"""Ensure that we handle posting a file."""
|
"""Ensure that we handle posting a file."""
|
||||||
url = httpbin_both + "/post"
|
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
|
original_response = requests.post(url, f).content
|
||||||
|
|
||||||
# This also tests that we do the right thing with matching the body when they are files.
|
# This also tests that we do the right thing with matching the body when they are files.
|
||||||
@@ -272,10 +273,10 @@ def test_post_file(tmpdir, httpbin_both):
|
|||||||
str(tmpdir.join("post_file.yaml")),
|
str(tmpdir.join("post_file.yaml")),
|
||||||
match_on=("method", "scheme", "host", "port", "path", "query", "body"),
|
match_on=("method", "scheme", "host", "port", "path", "query", "body"),
|
||||||
) as cass:
|
) as cass:
|
||||||
with open("tox.ini", "rb") as f:
|
with open(".editorconfig", "rb") as f:
|
||||||
tox_content = f.read()
|
editorconfig = f.read()
|
||||||
assert cass.requests[0].body.read() == tox_content
|
assert cass.requests[0].body.read() == editorconfig
|
||||||
with open("tox.ini", "rb") as f:
|
with open(".editorconfig", "rb") as f:
|
||||||
new_response = requests.post(url, f).content
|
new_response = requests.post(url, f).content
|
||||||
assert original_response == new_response
|
assert original_response == new_response
|
||||||
|
|
||||||
|
|||||||
@@ -2,10 +2,10 @@ import http.client as httplib
|
|||||||
import json
|
import json
|
||||||
import zlib
|
import zlib
|
||||||
|
|
||||||
from assertions import assert_is_json_bytes
|
|
||||||
|
|
||||||
import vcr
|
import vcr
|
||||||
|
|
||||||
|
from ..assertions import assert_is_json_bytes
|
||||||
|
|
||||||
|
|
||||||
def _headers_are_case_insensitive(host, port):
|
def _headers_are_case_insensitive(host, port):
|
||||||
conn = httplib.HTTPConnection(host, port)
|
conn = httplib.HTTPConnection(host, port)
|
||||||
|
|||||||
@@ -1,19 +1,44 @@
|
|||||||
"""Test requests' interaction with vcr"""
|
"""Test requests' interaction with vcr"""
|
||||||
|
import asyncio
|
||||||
|
import functools
|
||||||
|
import inspect
|
||||||
import json
|
import json
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from assertions import assert_cassette_empty, assert_is_json_bytes
|
|
||||||
|
|
||||||
import vcr
|
import vcr
|
||||||
from vcr.errors import CannotOverwriteExistingCassetteException
|
from vcr.errors import CannotOverwriteExistingCassetteException
|
||||||
|
|
||||||
|
from ..assertions import assert_cassette_empty, assert_is_json_bytes
|
||||||
|
|
||||||
tornado = pytest.importorskip("tornado")
|
tornado = pytest.importorskip("tornado")
|
||||||
|
gen = pytest.importorskip("tornado.gen")
|
||||||
http = pytest.importorskip("tornado.httpclient")
|
http = pytest.importorskip("tornado.httpclient")
|
||||||
|
|
||||||
# whether the current version of Tornado supports the raise_error argument for
|
# whether the current version of Tornado supports the raise_error argument for
|
||||||
# fetch().
|
# fetch().
|
||||||
supports_raise_error = tornado.version_info >= (4,)
|
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"])
|
@pytest.fixture(params=["simple", "curl", "default"])
|
||||||
@@ -43,7 +68,8 @@ def post(client, url, data=None, **kwargs):
|
|||||||
return client.fetch(http.HTTPRequest(url, method="POST", **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):
|
def test_status_code(get_client, scheme, tmpdir):
|
||||||
"""Ensure that we can read the status code"""
|
"""Ensure that we can read the status code"""
|
||||||
url = scheme + "://httpbin.org/"
|
url = scheme + "://httpbin.org/"
|
||||||
@@ -55,7 +81,8 @@ def test_status_code(get_client, scheme, tmpdir):
|
|||||||
assert 1 == cass.play_count
|
assert 1 == cass.play_count
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.gen_test
|
@pytest.mark.online
|
||||||
|
@gen_test
|
||||||
def test_headers(get_client, scheme, tmpdir):
|
def test_headers(get_client, scheme, tmpdir):
|
||||||
"""Ensure that we can read the headers back"""
|
"""Ensure that we can read the headers back"""
|
||||||
url = scheme + "://httpbin.org/"
|
url = scheme + "://httpbin.org/"
|
||||||
@@ -67,7 +94,8 @@ def test_headers(get_client, scheme, tmpdir):
|
|||||||
assert 1 == cass.play_count
|
assert 1 == cass.play_count
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.gen_test
|
@pytest.mark.online
|
||||||
|
@gen_test
|
||||||
def test_body(get_client, tmpdir, scheme):
|
def test_body(get_client, tmpdir, scheme):
|
||||||
"""Ensure the responses are all identical enough"""
|
"""Ensure the responses are all identical enough"""
|
||||||
|
|
||||||
@@ -80,7 +108,7 @@ def test_body(get_client, tmpdir, scheme):
|
|||||||
assert 1 == cass.play_count
|
assert 1 == cass.play_count
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.gen_test
|
@gen_test
|
||||||
def test_effective_url(get_client, tmpdir, httpbin):
|
def test_effective_url(get_client, tmpdir, httpbin):
|
||||||
"""Ensure that the effective_url is captured"""
|
"""Ensure that the effective_url is captured"""
|
||||||
url = httpbin.url + "/redirect/1"
|
url = httpbin.url + "/redirect/1"
|
||||||
@@ -93,7 +121,8 @@ def test_effective_url(get_client, tmpdir, httpbin):
|
|||||||
assert 1 == cass.play_count
|
assert 1 == cass.play_count
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.gen_test
|
@pytest.mark.online
|
||||||
|
@gen_test
|
||||||
def test_auth(get_client, tmpdir, scheme):
|
def test_auth(get_client, tmpdir, scheme):
|
||||||
"""Ensure that we can handle basic auth"""
|
"""Ensure that we can handle basic auth"""
|
||||||
auth = ("user", "passwd")
|
auth = ("user", "passwd")
|
||||||
@@ -108,7 +137,8 @@ def test_auth(get_client, tmpdir, scheme):
|
|||||||
assert 1 == cass.play_count
|
assert 1 == cass.play_count
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.gen_test
|
@pytest.mark.online
|
||||||
|
@gen_test
|
||||||
def test_auth_failed(get_client, tmpdir, scheme):
|
def test_auth_failed(get_client, tmpdir, scheme):
|
||||||
"""Ensure that we can save failed auth statuses"""
|
"""Ensure that we can save failed auth statuses"""
|
||||||
auth = ("user", "wrongwrongwrong")
|
auth = ("user", "wrongwrongwrong")
|
||||||
@@ -131,7 +161,8 @@ def test_auth_failed(get_client, tmpdir, scheme):
|
|||||||
assert 1 == cass.play_count
|
assert 1 == cass.play_count
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.gen_test
|
@pytest.mark.online
|
||||||
|
@gen_test
|
||||||
def test_post(get_client, tmpdir, scheme):
|
def test_post(get_client, tmpdir, scheme):
|
||||||
"""Ensure that we can post and cache the results"""
|
"""Ensure that we can post and cache the results"""
|
||||||
data = {"key1": "value1", "key2": "value2"}
|
data = {"key1": "value1", "key2": "value2"}
|
||||||
@@ -146,10 +177,10 @@ def test_post(get_client, tmpdir, scheme):
|
|||||||
assert 1 == cass.play_count
|
assert 1 == cass.play_count
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.gen_test
|
@gen_test
|
||||||
def test_redirects(get_client, tmpdir, scheme):
|
def test_redirects(get_client, tmpdir, httpbin):
|
||||||
"""Ensure that we can handle redirects"""
|
"""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"))):
|
with vcr.use_cassette(str(tmpdir.join("requests.yaml"))):
|
||||||
content = (yield get(get_client(), url)).body
|
content = (yield get(get_client(), url)).body
|
||||||
|
|
||||||
@@ -158,7 +189,8 @@ def test_redirects(get_client, tmpdir, scheme):
|
|||||||
assert cass.play_count == 1
|
assert cass.play_count == 1
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.gen_test
|
@pytest.mark.online
|
||||||
|
@gen_test
|
||||||
def test_cross_scheme(get_client, tmpdir, scheme):
|
def test_cross_scheme(get_client, tmpdir, scheme):
|
||||||
"""Ensure that requests between schemes are treated separately"""
|
"""Ensure that requests between schemes are treated separately"""
|
||||||
# First fetch a url under http, and then again under https and then
|
# First fetch a url under http, and then again under https and then
|
||||||
@@ -177,7 +209,8 @@ def test_cross_scheme(get_client, tmpdir, scheme):
|
|||||||
assert cass.play_count == 2
|
assert cass.play_count == 2
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.gen_test
|
@pytest.mark.online
|
||||||
|
@gen_test
|
||||||
def test_gzip(get_client, tmpdir, scheme):
|
def test_gzip(get_client, tmpdir, scheme):
|
||||||
"""
|
"""
|
||||||
Ensure that httpclient is able to automatically decompress the response
|
Ensure that httpclient is able to automatically decompress the response
|
||||||
@@ -202,7 +235,8 @@ def test_gzip(get_client, tmpdir, scheme):
|
|||||||
assert 1 == cass.play_count
|
assert 1 == cass.play_count
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.gen_test
|
@pytest.mark.online
|
||||||
|
@gen_test
|
||||||
def test_https_with_cert_validation_disabled(get_client, tmpdir):
|
def test_https_with_cert_validation_disabled(get_client, tmpdir):
|
||||||
cass_path = str(tmpdir.join("cert_validation_disabled.yaml"))
|
cass_path = str(tmpdir.join("cert_validation_disabled.yaml"))
|
||||||
|
|
||||||
@@ -214,7 +248,7 @@ def test_https_with_cert_validation_disabled(get_client, tmpdir):
|
|||||||
assert 1 == cass.play_count
|
assert 1 == cass.play_count
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.gen_test
|
@gen_test
|
||||||
def test_unsupported_features_raises_in_future(get_client, tmpdir):
|
def test_unsupported_features_raises_in_future(get_client, tmpdir):
|
||||||
"""Ensure that the exception for an AsyncHTTPClient feature not being
|
"""Ensure that the exception for an AsyncHTTPClient feature not being
|
||||||
supported is raised inside the future."""
|
supported is raised inside the future."""
|
||||||
@@ -232,7 +266,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.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):
|
def test_unsupported_features_raise_error_disabled(get_client, tmpdir):
|
||||||
"""Ensure that the exception for an AsyncHTTPClient feature not being
|
"""Ensure that the exception for an AsyncHTTPClient feature not being
|
||||||
supported is not raised if raise_error=False."""
|
supported is not raised if raise_error=False."""
|
||||||
@@ -251,7 +289,8 @@ def test_unsupported_features_raise_error_disabled(get_client, tmpdir):
|
|||||||
assert "not yet supported by VCR" in str(response.error)
|
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):
|
def test_cannot_overwrite_cassette_raises_in_future(get_client, tmpdir):
|
||||||
"""Ensure that CannotOverwriteExistingCassetteException is raised inside
|
"""Ensure that CannotOverwriteExistingCassetteException is raised inside
|
||||||
the future."""
|
the future."""
|
||||||
@@ -267,7 +306,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.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):
|
def test_cannot_overwrite_cassette_raise_error_disabled(get_client, tmpdir):
|
||||||
"""Ensure that CannotOverwriteExistingCassetteException is not raised if
|
"""Ensure that CannotOverwriteExistingCassetteException is not raised if
|
||||||
raise_error=False in the fetch() call."""
|
raise_error=False in the fetch() call."""
|
||||||
@@ -281,14 +324,14 @@ def test_cannot_overwrite_cassette_raise_error_disabled(get_client, tmpdir):
|
|||||||
assert isinstance(response.error, CannotOverwriteExistingCassetteException)
|
assert isinstance(response.error, CannotOverwriteExistingCassetteException)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.gen_test
|
@gen_test
|
||||||
@vcr.use_cassette(path_transformer=vcr.default_vcr.ensure_suffix(".yaml"))
|
@vcr.use_cassette(path_transformer=vcr.default_vcr.ensure_suffix(".yaml"))
|
||||||
def test_tornado_with_decorator_use_cassette(get_client):
|
def test_tornado_with_decorator_use_cassette(get_client):
|
||||||
response = yield get_client().fetch(http.HTTPRequest("http://www.google.com/", method="GET"))
|
response = yield get_client().fetch(http.HTTPRequest("http://www.google.com/", method="GET"))
|
||||||
assert response.body.decode("utf-8") == "not actually google"
|
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"))
|
@vcr.use_cassette(path_transformer=vcr.default_vcr.ensure_suffix(".yaml"))
|
||||||
def test_tornado_exception_can_be_caught(get_client):
|
def test_tornado_exception_can_be_caught(get_client):
|
||||||
try:
|
try:
|
||||||
@@ -302,7 +345,8 @@ def test_tornado_exception_can_be_caught(get_client):
|
|||||||
assert e.code == 404
|
assert e.code == 404
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.gen_test
|
@pytest.mark.online
|
||||||
|
@gen_test
|
||||||
def test_existing_references_get_patched(tmpdir):
|
def test_existing_references_get_patched(tmpdir):
|
||||||
from tornado.httpclient import AsyncHTTPClient
|
from tornado.httpclient import AsyncHTTPClient
|
||||||
|
|
||||||
@@ -315,7 +359,8 @@ def test_existing_references_get_patched(tmpdir):
|
|||||||
assert cass.play_count == 1
|
assert cass.play_count == 1
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.gen_test
|
@pytest.mark.online
|
||||||
|
@gen_test
|
||||||
def test_existing_instances_get_patched(get_client, tmpdir):
|
def test_existing_instances_get_patched(get_client, tmpdir):
|
||||||
"""Ensure that existing instances of AsyncHTTPClient get patched upon
|
"""Ensure that existing instances of AsyncHTTPClient get patched upon
|
||||||
entering VCR context."""
|
entering VCR context."""
|
||||||
@@ -330,7 +375,8 @@ def test_existing_instances_get_patched(get_client, tmpdir):
|
|||||||
assert cass.play_count == 1
|
assert cass.play_count == 1
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.gen_test
|
@pytest.mark.online
|
||||||
|
@gen_test
|
||||||
def test_request_time_is_set(get_client, tmpdir):
|
def test_request_time_is_set(get_client, tmpdir):
|
||||||
"""Ensures that the request_time on HTTPResponses is set."""
|
"""Ensures that the request_time on HTTPResponses is set."""
|
||||||
|
|
||||||
|
|||||||
@@ -5,12 +5,13 @@ from urllib.parse import urlencode
|
|||||||
from urllib.request import urlopen
|
from urllib.request import urlopen
|
||||||
|
|
||||||
import pytest_httpbin.certs
|
import pytest_httpbin.certs
|
||||||
from assertions import assert_cassette_has_one_response
|
|
||||||
from pytest import mark
|
from pytest import mark
|
||||||
|
|
||||||
# Internal imports
|
# Internal imports
|
||||||
import vcr
|
import vcr
|
||||||
|
|
||||||
|
from ..assertions import assert_cassette_has_one_response
|
||||||
|
|
||||||
|
|
||||||
def urlopen_with_cafile(*args, **kwargs):
|
def urlopen_with_cafile(*args, **kwargs):
|
||||||
context = ssl.create_default_context(cafile=pytest_httpbin.certs.where())
|
context = ssl.create_default_context(cafile=pytest_httpbin.certs.where())
|
||||||
|
|||||||
@@ -4,12 +4,13 @@
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
import pytest_httpbin
|
import pytest_httpbin
|
||||||
from assertions import assert_cassette_empty, assert_is_json_bytes
|
|
||||||
|
|
||||||
import vcr
|
import vcr
|
||||||
from vcr.patch import force_reset
|
from vcr.patch import force_reset
|
||||||
from vcr.stubs.compat import get_headers
|
from vcr.stubs.compat import get_headers
|
||||||
|
|
||||||
|
from ..assertions import assert_cassette_empty, assert_is_json_bytes
|
||||||
|
|
||||||
urllib3 = pytest.importorskip("urllib3")
|
urllib3 = pytest.importorskip("urllib3")
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -63,12 +63,12 @@ def test_flickr_should_respond_with_200(tmpdir):
|
|||||||
def test_cookies(tmpdir, httpbin):
|
def test_cookies(tmpdir, httpbin):
|
||||||
testfile = str(tmpdir.join("cookies.yml"))
|
testfile = str(tmpdir.join("cookies.yml"))
|
||||||
with vcr.use_cassette(testfile):
|
with vcr.use_cassette(testfile):
|
||||||
s = requests.Session()
|
with requests.Session() as s:
|
||||||
s.get(httpbin.url + "/cookies/set?k1=v1&k2=v2")
|
s.get(httpbin.url + "/cookies/set?k1=v1&k2=v2")
|
||||||
assert s.cookies.keys() == ["k1", "k2"]
|
assert s.cookies.keys() == ["k1", "k2"]
|
||||||
|
|
||||||
r2 = s.get(httpbin.url + "/cookies")
|
r2 = s.get(httpbin.url + "/cookies")
|
||||||
assert sorted(r2.json()["cookies"].keys()) == ["k1", "k2"]
|
assert sorted(r2.json()["cookies"].keys()) == ["k1", "k2"]
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.online
|
@pytest.mark.online
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import contextlib
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
|
||||||
from pytest import mark
|
from pytest import mark
|
||||||
@@ -16,7 +17,7 @@ class TestVCRConnection:
|
|||||||
@mark.online
|
@mark.online
|
||||||
@mock.patch("vcr.cassette.Cassette.can_play_response_for", return_value=False)
|
@mock.patch("vcr.cassette.Cassette.can_play_response_for", return_value=False)
|
||||||
def testing_connect(*args):
|
def testing_connect(*args):
|
||||||
vcr_connection = VCRHTTPSConnection("www.google.com")
|
with contextlib.closing(VCRHTTPSConnection("www.google.com")) as vcr_connection:
|
||||||
vcr_connection.cassette = Cassette("test", record_mode=mode.ALL)
|
vcr_connection.cassette = Cassette("test", record_mode=mode.ALL)
|
||||||
vcr_connection.real_connection.connect()
|
vcr_connection.real_connection.connect()
|
||||||
assert vcr_connection.real_connection.sock is not None
|
assert vcr_connection.real_connection.sock is not None
|
||||||
|
|||||||
87
tox.ini
87
tox.ini
@@ -1,87 +0,0 @@
|
|||||||
[tox]
|
|
||||||
skip_missing_interpreters=true
|
|
||||||
envlist =
|
|
||||||
cov-clean,
|
|
||||||
lint,
|
|
||||||
{py38,py39,py310,py311,py312}-{requests-urllib3-1,httplib2,urllib3-1,tornado4,boto3,aiohttp,httpx},
|
|
||||||
{py310,py311,py312}-{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
|
|
||||||
3.12: py312
|
|
||||||
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,py312,pypy3}-{requests-urllib3-1,httplib2,urllib3-1,tornado4,boto3},{py310,py311,py312}-{requests-urllib3-2,urllib3-2},{py38,py39,py310,py311,py312}-{aiohttp},{py38,py39,py310,py311,py312}-{httpx}: cov-clean
|
|
||||||
cov-report: lint,{py38,py39,py310,py311,py312,pypy3}-{requests-urllib3-1,httplib2,urllib3-1,tornado4,boto3},{py310,py311,py312}-{requests-urllib3-2,urllib3-2},{py38,py39,py310,py311,py312}-{aiohttp}
|
|
||||||
passenv =
|
|
||||||
AWS_ACCESS_KEY_ID
|
|
||||||
AWS_DEFAULT_REGION
|
|
||||||
AWS_SECRET_ACCESS_KEY
|
|
||||||
setenv =
|
|
||||||
# workaround for broken C extension in aiohttp
|
|
||||||
# see: https://github.com/aio-libs/aiohttp/issues/7229
|
|
||||||
py312: AIOHTTP_NO_EXTENSIONS=1
|
|
||||||
@@ -4,7 +4,7 @@ from logging import NullHandler
|
|||||||
from .config import VCR
|
from .config import VCR
|
||||||
from .record_mode import RecordMode as mode # noqa: F401
|
from .record_mode import RecordMode as mode # noqa: F401
|
||||||
|
|
||||||
__version__ = "5.1.0"
|
__version__ = "6.0.0"
|
||||||
|
|
||||||
logging.getLogger(__name__).addHandler(NullHandler())
|
logging.getLogger(__name__).addHandler(NullHandler())
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import contextlib
|
|||||||
import copy
|
import copy
|
||||||
import inspect
|
import inspect
|
||||||
import logging
|
import logging
|
||||||
import sys
|
|
||||||
from asyncio import iscoroutinefunction
|
from asyncio import iscoroutinefunction
|
||||||
|
|
||||||
import wrapt
|
import wrapt
|
||||||
@@ -126,20 +125,7 @@ class CassetteContextDecorator:
|
|||||||
duration of the generator.
|
duration of the generator.
|
||||||
"""
|
"""
|
||||||
with self as cassette:
|
with self as cassette:
|
||||||
coroutine = fn(cassette)
|
yield from 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
|
|
||||||
|
|
||||||
def _handle_function(self, fn):
|
def _handle_function(self, fn):
|
||||||
with self as cassette:
|
with self as cassette:
|
||||||
|
|||||||
19
vcr/patch.py
19
vcr/patch.py
@@ -260,10 +260,14 @@ class CassettePatcherBuilder:
|
|||||||
|
|
||||||
yield cpool, "HTTPConnectionWithTimeout", VCRHTTPConnectionWithTimeout
|
yield cpool, "HTTPConnectionWithTimeout", VCRHTTPConnectionWithTimeout
|
||||||
yield cpool, "HTTPSConnectionWithTimeout", VCRHTTPSConnectionWithTimeout
|
yield cpool, "HTTPSConnectionWithTimeout", VCRHTTPSConnectionWithTimeout
|
||||||
yield cpool, "SCHEME_TO_CONNECTION", {
|
yield (
|
||||||
"http": VCRHTTPConnectionWithTimeout,
|
cpool,
|
||||||
"https": VCRHTTPSConnectionWithTimeout,
|
"SCHEME_TO_CONNECTION",
|
||||||
}
|
{
|
||||||
|
"http": VCRHTTPConnectionWithTimeout,
|
||||||
|
"https": VCRHTTPSConnectionWithTimeout,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
@_build_patchers_from_mock_triples_decorator
|
@_build_patchers_from_mock_triples_decorator
|
||||||
def _tornado(self):
|
def _tornado(self):
|
||||||
@@ -368,10 +372,6 @@ class ConnectionRemover:
|
|||||||
if isinstance(connection, self._connection_class):
|
if isinstance(connection, self._connection_class):
|
||||||
self._connection_pool_to_connections.setdefault(pool, set()).add(connection)
|
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):
|
def __enter__(self):
|
||||||
return self
|
return self
|
||||||
|
|
||||||
@@ -382,10 +382,13 @@ class ConnectionRemover:
|
|||||||
connection = pool.pool.get()
|
connection = pool.pool.get()
|
||||||
if isinstance(connection, self._connection_class):
|
if isinstance(connection, self._connection_class):
|
||||||
connections.remove(connection)
|
connections.remove(connection)
|
||||||
|
connection.close()
|
||||||
else:
|
else:
|
||||||
readd_connections.append(connection)
|
readd_connections.append(connection)
|
||||||
for connection in readd_connections:
|
for connection in readd_connections:
|
||||||
pool._put_conn(connection)
|
pool._put_conn(connection)
|
||||||
|
for connection in connections:
|
||||||
|
connection.close()
|
||||||
|
|
||||||
|
|
||||||
def reset_patchers():
|
def reset_patchers():
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import asyncio
|
||||||
import functools
|
import functools
|
||||||
import inspect
|
import inspect
|
||||||
import logging
|
import logging
|
||||||
@@ -6,7 +7,9 @@ from unittest.mock import MagicMock, patch
|
|||||||
import httpx
|
import httpx
|
||||||
|
|
||||||
from vcr.errors import CannotOverwriteExistingCassetteException
|
from vcr.errors import CannotOverwriteExistingCassetteException
|
||||||
|
from vcr.filters import decode_response
|
||||||
from vcr.request import Request as VcrRequest
|
from vcr.request import Request as VcrRequest
|
||||||
|
from vcr.serializers.compat import convert_body_to_bytes
|
||||||
|
|
||||||
_httpx_signature = inspect.signature(httpx.Client.request)
|
_httpx_signature = inspect.signature(httpx.Client.request)
|
||||||
|
|
||||||
@@ -33,14 +36,29 @@ def _transform_headers(httpx_response):
|
|||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
def _to_serialized_response(httpx_response):
|
async def _to_serialized_response(resp, aread):
|
||||||
return {
|
# The content shouldn't already have been read in by HTTPX.
|
||||||
"status_code": httpx_response.status_code,
|
assert not hasattr(resp, "_decoder")
|
||||||
"http_version": httpx_response.http_version,
|
|
||||||
"headers": _transform_headers(httpx_response),
|
# Retrieve the content, but without decoding it.
|
||||||
"content": httpx_response.content,
|
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):
|
def _from_serialized_headers(headers):
|
||||||
"""
|
"""
|
||||||
@@ -57,15 +75,32 @@ def _from_serialized_headers(headers):
|
|||||||
@patch("httpx.Response.close", MagicMock())
|
@patch("httpx.Response.close", MagicMock())
|
||||||
@patch("httpx.Response.read", MagicMock())
|
@patch("httpx.Response.read", MagicMock())
|
||||||
def _from_serialized_response(request, serialized_response, history=None):
|
def _from_serialized_response(request, serialized_response, history=None):
|
||||||
content = serialized_response.get("content")
|
# 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"]},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
extensions = None
|
||||||
|
else:
|
||||||
|
extensions = {"reason_phrase": serialized_response["status"]["message"].encode()}
|
||||||
|
|
||||||
response = httpx.Response(
|
response = httpx.Response(
|
||||||
status_code=serialized_response.get("status_code"),
|
status_code=serialized_response["status"]["code"],
|
||||||
request=request,
|
request=request,
|
||||||
headers=_from_serialized_headers(serialized_response.get("headers")),
|
headers=_from_serialized_headers(serialized_response["headers"]),
|
||||||
content=content,
|
content=serialized_response["body"]["string"],
|
||||||
history=history or [],
|
history=history or [],
|
||||||
|
extensions=extensions,
|
||||||
)
|
)
|
||||||
response._content = content
|
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
@@ -91,17 +126,17 @@ def _shared_vcr_send(cassette, real_send, *args, **kwargs):
|
|||||||
return vcr_request, None
|
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:
|
for past_real_response in real_response.history:
|
||||||
past_vcr_request = _make_vcr_request(past_real_response.request)
|
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 real_response.history:
|
||||||
# If there was a redirection keep we want the request which will hold the
|
# If there was a redirection keep we want the request which will hold the
|
||||||
# final redirect value
|
# final redirect value
|
||||||
vcr_request = _make_vcr_request(real_response.request)
|
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
|
return real_response
|
||||||
|
|
||||||
|
|
||||||
@@ -119,8 +154,8 @@ async def _async_vcr_send(cassette, real_send, *args, **kwargs):
|
|||||||
return response
|
return response
|
||||||
|
|
||||||
real_response = await real_send(*args, **kwargs)
|
real_response = await real_send(*args, **kwargs)
|
||||||
await real_response.aread()
|
await _record_responses(cassette, vcr_request, real_response, aread=True)
|
||||||
return _record_responses(cassette, vcr_request, real_response)
|
return real_response
|
||||||
|
|
||||||
|
|
||||||
def async_vcr_send(cassette, real_send):
|
def async_vcr_send(cassette, real_send):
|
||||||
@@ -139,8 +174,8 @@ def _sync_vcr_send(cassette, real_send, *args, **kwargs):
|
|||||||
return response
|
return response
|
||||||
|
|
||||||
real_response = real_send(*args, **kwargs)
|
real_response = real_send(*args, **kwargs)
|
||||||
real_response.read()
|
asyncio.run(_record_responses(cassette, vcr_request, real_response, aread=False))
|
||||||
return _record_responses(cassette, vcr_request, real_response)
|
return real_response
|
||||||
|
|
||||||
|
|
||||||
def sync_vcr_send(cassette, real_send):
|
def sync_vcr_send(cassette, real_send):
|
||||||
|
|||||||
Reference in New Issue
Block a user