1
0
mirror of https://github.com/kevin1024/vcrpy.git synced 2025-12-08 16:53:23 +00:00

Compare commits

...

116 Commits

Author SHA1 Message Date
Kevin McCarthy
1e3a5ac753 Release v6.0.0 2024-01-23 10:54:46 -05:00
Thomas Grainger
3a5ff1c1ce Merge pull request #810 from graingert/fix-tornado-related-deprecation-warnings
fix tornado related warnings by replacing pytest-tornado with a simple coroutine runner
2024-01-23 14:02:31 +00:00
Thomas Grainger
bf80673454 fix tornado related warnings 2024-01-23 13:02:29 +00:00
Thomas Grainger
8028420cbb Merge pull request #801 from graingert/fix-resource-warning-2 2024-01-23 12:39:35 +00:00
Thomas Grainger
784b2dcb29 tornado test_redirects is no longer an online test 2024-01-23 12:33:45 +00:00
Thomas Grainger
42b4a5d2fa move off of mockbin on tornado tests also 2024-01-23 12:33:15 +00:00
Thomas Grainger
b7f6c2fce2 mark tornado tests as online 2024-01-23 12:24:48 +00:00
Thomas Grainger
6d7a842a33 fix test_tornado_exception_can_be_caught RuntimeError: generator raised StopIteration 2024-01-23 12:24:48 +00:00
Thomas Grainger
db1f5b0dee tornado 6 changes raise_error behaviour 2024-01-23 12:12:52 +00:00
Thomas Grainger
c6667ac56c restore scheme fixture for tests 2024-01-23 12:12:05 +00:00
Thomas Grainger
a093fb177d add new deprecation warnings for tornado tests 2024-01-23 12:09:39 +00:00
Thomas Grainger
666686b542 restore pytest-tornado 2024-01-23 11:12:02 +00:00
Thomas Grainger
5104b1f462 Merge branch 'master' of github.com:kevin1024/vcrpy into fix-resource-warning-2 2024-01-23 11:03:49 +00:00
Jair Henrique
62fe272a8e Remove tox reference from contributing docs 2024-01-22 23:17:25 -03:00
Jair Henrique
f9b69d8da7 Remove tox reference from runtests.sh file 2024-01-22 23:17:25 -03:00
Jair Henrique
cb77cb8f69 Remove tox.ini file 2024-01-22 23:17:25 -03:00
Jair Henrique
e37fc9ab6e Refactor ci main workflow to use matrix to run tests without tox 2024-01-22 23:17:25 -03:00
Jair Henrique
abbb50135f Organize dependencies for tests and extras requires 2024-01-22 23:17:25 -03:00
Jair Henrique
0594de9b3e Remove tox.ini from MANIFEST.in file 2024-01-22 23:17:25 -03:00
Jair Henrique
53f686aa5b Refactor test to not use tox.ini file 2024-01-22 23:17:25 -03:00
pre-commit
1677154f04 pre-commit: Autoupdate 2024-01-22 23:14:05 -03:00
Allan Crooks
54bc6467eb Run linters. 2024-01-22 23:13:10 -03:00
Allan Crooks
c5487384ee Fix handling of encoded content in HTTPX stub.
Also copied over and adjusted some of the tests from
test_requests.py relating to gzipped handling to show
that the HTTPX stub is behaving in a consistent way to
how the requests stub is.
2024-01-22 23:13:10 -03:00
Allan Crooks
5cf23298ac HTTPX stub now generates cassettes in the same format as other stubs.
As part of this, I've removed the tests which inspect the
data type of the response content in the cassette. That
behaviour should be controlled via the inbuilt serializers.
2024-01-22 23:13:10 -03:00
Allan Crooks
5fa7010712 Allow HTTPX stub to read cassettes generated by other stubs.
This was due to a custom format being defined in the HTTPX stub.
2024-01-22 23:13:10 -03:00
pre-commit
f1e0241673 pre-commit: Autoupdate 2024-01-05 16:36:48 -03:00
pre-commit
a3a255d606 pre-commit: Autoupdate 2024-01-02 10:58:59 -03:00
dependabot[bot]
0782382982 build(deps): bump actions/checkout from 3 to 4
Bumps [actions/checkout](https://github.com/actions/checkout) from 3 to 4.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v3...v4)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-12-18 12:16:06 -03:00
dependabot[bot]
395d2be295 build(deps): bump actions/setup-python from 4 to 5
Bumps [actions/setup-python](https://github.com/actions/setup-python) from 4 to 5.
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](https://github.com/actions/setup-python/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/setup-python
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-12-18 11:08:49 -03:00
Thomas Grainger
ee6e7905e9 Merge branch 'master' into fix-resource-warning-2 2023-12-15 19:13:39 +00:00
Thomas Grainger
cc4d03c62e close unremoved connections (pool already removed the connection) 2023-12-15 19:11:25 +00:00
Thomas Grainger
8e13af2ee9 use context manager for requests.Session 2023-12-15 18:38:56 +00:00
pre-commit
b522d3f0a3 pre-commit: Autoupdate 2023-12-15 14:46:28 -03:00
Thomas Grainger
d39c26b358 remember to close removed connections 2023-12-15 14:26:44 +00:00
Thomas Grainger
d76c243513 Revert "Fix ResourceWarning unclosed socket"
This reverts commit f4144359f6.
2023-12-15 14:01:14 +00:00
Thomas Grainger
5cff354ec8 Revert "fix a KeyError"
This reverts commit fa789e975b.
2023-12-15 14:01:10 +00:00
Thomas Grainger
80614dbd00 fix resource warning due to pytest-asyncio 2023-12-15 13:51:52 +00:00
Thomas Grainger
356ff4122c fix sync do_request().stream 2023-12-15 13:40:06 +00:00
Thomas Grainger
cf765928ac remove redundant contextlib import 2023-12-15 13:35:31 +00:00
Thomas Grainger
73d11e80eb fix httpx resource warnings 2023-12-15 13:34:52 +00:00
Thomas Grainger
97de8a0fce ignore warning from dateutil 2023-12-15 13:09:13 +00:00
Thomas Grainger
895ae205ca use asyncio.run to run coroutines 2023-12-15 11:39:51 +00:00
Thomas Grainger
f075c8b0b4 close aiohttp session on errors 2023-12-15 11:39:16 +00:00
Thomas Grainger
3919cb2573 remember to close the VCRHTTPSConnection 2023-12-15 11:08:49 +00:00
Thomas Grainger
bddec2e62a use socketserver.ThreadingTCPServer as a contextmanager 2023-12-15 11:08:31 +00:00
Thomas Grainger
fa789e975b fix a KeyError 2023-12-15 11:07:56 +00:00
Thomas Grainger
556fd0166c enable filterwarnings=error 2023-12-15 11:07:40 +00:00
Thomas Grainger
17c78bff9e Merge branch 'master' of github.com:kevin1024/vcrpy into fix-resource-warning-2 2023-12-15 10:48:27 +00:00
Sebastian Pipping
713cb36d35 Merge pull request #800 from kevin1024/pre-commit-ci
Start using pre-commit in CI
2023-12-12 20:28:19 +01:00
Sebastian Pipping
b0cb8765d5 pre-commit: Add --show-source to ruff
Suggested by Jair Henrique.
2023-12-12 20:01:09 +01:00
Sebastian Pipping
97ad51fe6c pre-commit: Enable trailing-whitespace 2023-12-12 20:01:09 +01:00
Sebastian Pipping
1dd9cbde8b pre-commit: Mass-apply trailing-whitespace 2023-12-12 20:01:09 +01:00
Sebastian Pipping
962284072b pre-commit: Enable end-of-file-fixer 2023-12-12 19:01:50 +01:00
Sebastian Pipping
e9102b2bb4 pre-commit: Mass-apply end-of-file-fixer 2023-12-12 19:01:50 +01:00
Sebastian Pipping
957c8bd7a3 pre-commit: Protect against accidental merge conflict markers 2023-12-12 19:01:50 +01:00
Sebastian Pipping
2d5f8a499e lint.yml: Drop as superseded by pre-commit.yml 2023-12-12 19:01:50 +01:00
Sebastian Pipping
e5555a5d5b pre-commit: Make CI keep keep the config up to date via pull requests 2023-12-12 19:01:50 +01:00
Sebastian Pipping
a542567e4a pre-commit: Integrate with GitHub Actions CI 2023-12-12 19:01:50 +01:00
Sebastian Pipping
3168e7813e pre-commit: Enable Ruff and Ruff Black-style formatting 2023-12-12 19:00:57 +01:00
Jair Henrique
88cf01aa14 Fix format code 2023-12-12 14:24:22 -03:00
Parker Hancock
85ae012d9c fix linting 2023-12-12 14:24:22 -03:00
Parker Hancock
db1e9e7180 make cassettes human readable 2023-12-12 14:24:22 -03:00
Jair Henrique
dbf7a3337b Show ruff diff errors 2023-12-12 13:58:25 -03:00
dependabot[bot]
dd97b02b72 build(deps): bump sphinx-rtd-theme from 1.3.0 to 2.0.0
Bumps [sphinx-rtd-theme](https://github.com/readthedocs/sphinx_rtd_theme) from 1.3.0 to 2.0.0.
- [Changelog](https://github.com/readthedocs/sphinx_rtd_theme/blob/master/docs/changelog.rst)
- [Commits](https://github.com/readthedocs/sphinx_rtd_theme/compare/1.3.0...2.0.0)

---
updated-dependencies:
- dependency-name: sphinx-rtd-theme
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-12-11 11:27:04 -03:00
dependabot[bot]
e8346ad30e build(deps): bump actions/setup-python from 4 to 5
Bumps [actions/setup-python](https://github.com/actions/setup-python) from 4 to 5.
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](https://github.com/actions/setup-python/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/setup-python
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-12-11 10:42:13 -03:00
Sebastian Pipping
6a31904333 Merge pull request #789 from kevin1024/fix-imports-to-assertions-py
tests: Fix imports to `tests/assertions.py` (fixes #773)
2023-12-11 00:58:01 +01:00
Jair Henrique
796dc8de7e Move lint from tox to gh action 2023-12-10 20:48:01 -03:00
Sebastian Pipping
ecb5d84f0f tests: Fix imports to tests/assertions.py 2023-12-11 00:36:46 +01:00
Jair Henrique
cebdd45849 Use ruff to check code format intead of black 2023-12-10 20:12:09 -03:00
Sebastian Pipping
8a8d46f130 Merge pull request #775 from kevin1024/python-3-12
Finish up on Python 3.12 support
2023-12-10 23:49:46 +01:00
Sebastian Pipping
954a100dfd Finish up on Python 3.12 support 2023-12-10 23:33:30 +01:00
Sebastian Pipping
604c0be571 Merge pull request #787 from kevin1024/fix-pypy-3-10
Fix CI / Block urllib3 >=2 for PyPy (alternative to #786)
2023-12-10 23:32:23 +01:00
Sebastian Pipping
0e57182207 setup.py: Block urllib3 >=2 for PyPy (including 3.10)
It kept failing CI
2023-12-08 23:05:09 +01:00
Rob Brackett
c062c9f54c Remove spaces at end-of-line in changelog
This matches the project's `.editorconfig` rules.
2023-12-08 16:59:39 -03:00
Rob Brackett
2abf1188a9 Fix list formatting in v5.1.0 changelog
The list of changes was not indented enough, and so didn't actually get formatted as a list when rendering HTML, which left it pretty unreadable. This also adds a blank line between the last 4.x version and 5.0.0 to match the extra blank lines between other major versions.
2023-12-08 16:59:39 -03:00
dependabot[bot]
2b2935a1e7 build(deps): bump sphinx-rtd-theme from 1.2.2 to 1.3.0
Bumps [sphinx-rtd-theme](https://github.com/readthedocs/sphinx_rtd_theme) from 1.2.2 to 1.3.0.
- [Changelog](https://github.com/readthedocs/sphinx_rtd_theme/blob/master/docs/changelog.rst)
- [Commits](https://github.com/readthedocs/sphinx_rtd_theme/compare/1.2.2...1.3.0)

---
updated-dependencies:
- dependency-name: sphinx-rtd-theme
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-12-08 16:39:02 -03:00
dependabot[bot]
a8545c89a5 build(deps): bump actions/checkout from 3 to 4
Bumps [actions/checkout](https://github.com/actions/checkout) from 3 to 4.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v3...v4)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-12-08 16:38:47 -03:00
Parker Hancock
5532c0b4cf more attempts to make the linters happy 2023-12-08 16:38:33 -03:00
Parker Hancock
f4467a8d6c make linters happy 2023-12-08 16:38:33 -03:00
Parker Hancock
f5fc7aac22 fix tests 2023-12-08 16:38:33 -03:00
Parker Hancock
e8e9a4af9f remove unnecssary comment 2023-12-08 16:38:33 -03:00
Parker Hancock
7bf8f65815 fixes for httpx 2023-12-08 16:38:33 -03:00
Michał Górny
defad28771 Disable C extension in aiohttp to fix Python 3.12 install
Disable the C extension in aiohttp that's incompatible with Python 3.12
as of 3.8.5, in order to make it possible to install it (in pure Python
version) for testing.
2023-08-09 10:09:12 -03:00
Michał Górny
69621c67fb Copy debuglevel and _http_vsn attrs into response classes
Copy the `debuglevel` and `_http_vsn` attributes from base connection
class into response classes, in order to fix compatibility with
Python 3.12.  For reasons I don't comprehend, these end up being called
on the class rather than instance, so regular proxying logic does not
work.

Fixes #707
2023-08-09 10:09:12 -03:00
Michał Górny
469a10b980 Enable testing on pypy-3.9 & 3.10 2023-08-09 10:09:12 -03:00
Michał Górny
d90cea0260 Enable testing on Python 3.12 2023-08-09 10:09:12 -03:00
Harmon
c9da7a102f Configure Read the Docs to install the library 2023-08-07 08:34:45 -03:00
Mohammad Razavi
f4144359f6 Fix ResourceWarning unclosed socket
This PR fixes issue #710 by properly closing the underlying socket. It
first uses `pool._put_conn` to keep the connection in the pool, and
later removes and closes it when the context manager exits.

I was unsure about the exact purpose of the `ConnectonRemove` class,
so I made minimal changes to minimize the risk of breaking the code
and there may be better solutions for fixing this issue.

For example, the `urllib3.connectionpool.HTTPConnectionPool` will
utilize a weakref to terminate pool connections. By appending our
connection to it, it will also take care of closing our connection. So
another solution could be to modify the `__exit__` in
`patch.ConnectionRemover` method and add our connection to the pool:

```py
class ConnectionRemover:
    ...

    def __exit__(self, *args):
        for pool, connections in self._connection_pool_to_connections.items():
            for connection in connections:
                if isinstance(connection, self._connection_class):
                    pool._put_conn(connection)
```
2023-08-07 08:42:58 +02:00
Jair Henrique
69de388649 Drop simplejson support 2023-08-01 08:53:31 -03:00
Jair Henrique
6446d00e27 Drop boto 2 support 2023-07-31 08:49:23 -03:00
Kevin McCarthy
d6bded1820 bump version to v5.1.0 2023-07-30 17:11:15 -10:00
Sebastian Pipping
e7c00a4bf9 Merge pull request #739 from kevin1024/issue-734-fix-body-matcher-for-chunked-requests
Fix body matcher for chunked requests (fixes #734)
2023-07-23 23:22:34 +02:00
dependabot[bot]
92dd4d00f7 build(deps): update sphinx requirement from <7 to <8
Updates the requirements on [sphinx](https://github.com/sphinx-doc/sphinx) to permit the latest version.
- [Release notes](https://github.com/sphinx-doc/sphinx/releases)
- [Changelog](https://github.com/sphinx-doc/sphinx/blob/master/CHANGES)
- [Commits](https://github.com/sphinx-doc/sphinx/compare/v0.1.61611...v7.0.1)

---
updated-dependencies:
- dependency-name: sphinx
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-07-23 18:19:26 -03:00
Jair Henrique
cf3ffcad61 Create action to validate docs 2023-07-23 18:09:49 -03:00
Jair Henrique
3ad462e766 Drop dependabot duplicity check 2023-07-23 18:09:49 -03:00
Jair Henrique
cdab3fcb30 Drop iscoroutinefunction fallback function for unsupported python 2023-07-23 12:30:58 -03:00
Jair Henrique
d3a5f4dd6c Add editorconfig file 2023-07-23 12:03:42 -03:00
Jair Henrique
75c8607fd2 Fix read the docs build 2023-07-20 13:17:15 -03:00
Jair Henrique
8c075c7fb3 Configure read the docs V2 2023-07-20 13:10:07 -03:00
Sebastian Pipping
a045a46bb4 Merge pull request #740 from kevin1024/issue-512-fix-query-param-filter-for-aiohttp
Fix query param filter for aiohttp (fixes #517)
2023-07-19 15:37:54 +02:00
Sebastian Pipping
1d979b078d Merge pull request #743 from charettes/remove-six
Remove unnecessary dependency on six.
2023-07-18 23:11:31 +02:00
Simon Charette
f7d76bd40a Remove unnecessary dependency on six.
Remove the last remaining usag of it in VCR.testcase.
2023-07-16 22:22:34 -04:00
Sebastian Pipping
7e11cfc9e4 Merge pull request #738 from kevin1024/json-loads-py36-plus
Make `json.loads` of Python >=3.6 decode bytes by itself
2023-07-11 17:30:39 +02:00
Sebastian Pipping
c95b7264a2 Merge pull request #735 from quasimik/patch-1
Fix typo in docs
2023-07-10 18:52:30 +02:00
Sebastian Pipping
8ab8e63e04 test_filter.py: Make test_filter_querystring meaner 2023-07-10 16:34:09 +02:00
Sebastian Pipping
d2c1da9ab7 test_aiohttp.py: Cover filter_query_parameters with aiohttp 2023-07-10 16:34:08 +02:00
Sebastian Pipping
8336d66976 aiohttp_stubs.py: Stop leaking unfiltered URL into cassette responses 2023-07-10 16:11:26 +02:00
Sebastian Pipping
e69b10c2e0 test_matchers.py: Briefly cover chunked transfer encoding 2023-07-08 02:25:51 +02:00
Sebastian Pipping
a6b9a070a5 matchers.py: Decode chunked request bodies 2023-07-08 02:25:51 +02:00
Sebastian Pipping
e35205c5c8 matchers.py: Support transforming the request body multiple times 2023-07-08 01:29:19 +02:00
Sebastian Pipping
05f61ea56c Make json.loads of Python >=3.6 decode bytes by itself
Quoting https://docs.python.org/3/library/json.html#json.loads :
> Changed in version 3.6: s can now be of type bytes or bytearray.
> The input encoding should be UTF-8, UTF-16 or UTF-32.
2023-07-07 20:00:57 +02:00
Michael Liu
943cabb14f docs/advanced.rst: fix typo 2023-07-04 16:45:49 +08:00
Jair Henrique
4f70152e7c Enable rule B (flake8-bugbear) on ruff 2023-06-27 17:36:26 -03:00
Jair Henrique
016a394f2c Enable E, W and F linters for ruff 2023-06-26 20:46:09 -03:00
Jair Henrique
6b2fc182c3 Improve string format 2023-06-26 20:46:09 -03:00
Jair Henrique
a77173c002 Use ruff as linter 2023-06-26 20:46:09 -03:00
79 changed files with 1239 additions and 927 deletions

14
.editorconfig Normal file
View File

@@ -0,0 +1,14 @@
root = true
[*]
indent_style = space
indent_size = 4
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[Makefile]
indent_style = tab
[*.{yml,yaml}]
indent_size = 2

View File

@@ -17,6 +17,6 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Codespell
uses: codespell-project/actions-codespell@v2

23
.github/workflows/docs.yml vendored Normal file
View File

@@ -0,0 +1,23 @@
name: Validate docs
on:
push:
paths:
- 'docs/**'
jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install build dependencies
run: pip install -r docs/requirements.txt
- name: Rendering HTML documentation
run: sphinx-build -b html docs/ html
- name: Inspect html rendered
run: cat html/index.html

View File

@@ -13,31 +13,56 @@ 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"
- "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
- name: Install project dependencies
run: |
pip3 install --upgrade pip
pip3 install codecov tox tox-gh-actions
pip install --upgrade pip
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

View 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
View 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
View 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

24
.readthedocs.yaml Normal file
View File

@@ -0,0 +1,24 @@
# .readthedocs.yaml
# Read the Docs configuration file
# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
# Required
version: 2
# Set the version of Python and other tools you might need
build:
os: ubuntu-22.04
tools:
python: "3.12"
# Build documentation in the docs/ directory with Sphinx
sphinx:
configuration: docs/conf.py
# We recommend specifying your dependencies to enable reproducible builds:
# https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html
python:
install:
- requirements: docs/requirements.txt
- method: pip
path: .

View File

@@ -1,6 +1,5 @@
include README.rst
include LICENSE.txt
include tox.ini
recursive-include tests *
recursive-exclude * __pycache__
recursive-exclude * *.py[co]

View File

@@ -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

View File

@@ -24,4 +24,4 @@
<stop offset="1" stop-color="#27DDA6"/>
</linearGradient>
</defs>
</svg>
</svg>

Before

Width:  |  Height:  |  Size: 6.2 KiB

After

Width:  |  Height:  |  Size: 6.2 KiB

View File

@@ -16,7 +16,7 @@ a nice addition. Here's an example:
with vcr.use_cassette('fixtures/vcr_cassettes/synopsis.yaml') as cass:
response = urllib2.urlopen('http://www.zombo.com/').read()
# 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/
assert cass.requests[0].uri == 'http://www.zombo.com/'
@@ -189,7 +189,7 @@ of post data parameters to filter.
.. code:: python
with my_vcr.use_cassette('test.yml', filter_post_data_parameters=['client_secret']):
with my_vcr.use_cassette('test.yml', filter_post_data_parameters=['api_key']):
requests.post('http://api.com/postdata', data={'api_key': 'secretstring'})
Advanced use of filter_headers, filter_query_parameters and filter_post_data_parameters
@@ -208,7 +208,7 @@ So these two calls are the same:
# original (still works)
vcr = VCR(filter_headers=['authorization'])
# new
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)
vcr = VCR(filter_headers=[('authorization', 'XXXXXX')])
# replace with a callable, for example when testing
# lots of different kinds of authorization.
def replace_auth(key, value, request):
@@ -286,7 +286,7 @@ sensitive data from the response body:
before_record_response=scrub_string(settings.USERNAME, 'username'),
)
with my_vcr.use_cassette('test.yml'):
# your http code here
# your http code here
Decode compressed response

View File

@@ -7,9 +7,31 @@ 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.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
- Configure read the docs V2 - thanks @jairhenrique
- Fix typo in docs - thanks @quasimik
- Make json.loads of Python >=3.6 decode bytes by itself - thanks @hartwork
- Fix body matcher for chunked requests (fixes #734) - thanks @hartwork
- Fix query param filter for aiohttp (fixes #517) - thanks @hartwork and @salomvary
- Remove unnecessary dependency on six. - thanks @charettes
- build(deps): update sphinx requirement from <7 to <8 - thanks @jairhenrique
- Add action to validate docs - thanks @jairhenrique
- Add editorconfig file - thanks @jairhenrique
- Drop iscoroutinefunction fallback function for unsupported python thanks @jairhenrique
- 5.0.0
- BREAKING CHANGE: Drop support for Python 3.7. 3.7 is EOL as of 6/27/23 Thanks @jairhenrique
- BREAKING CHANGE: Custom Cassette persisters no longer catch ValueError. If you have implemented a custom persister (has anyone implemented a custom persister? Let us know!) then you will need to throw a CassetteNotFoundError when unable to find a cassette. See #681 for discussion and reason for this change. Thanks @amosjyng for the PR and the review from @hartwork
- 4.4.0
- HUGE thanks to @hartwork for all the work done on this release!
- Bring vcr/unittest in to vcrpy as a full feature of vcr instead of a separate library. Big thanks to @hartwork for doing this and to @agriffis for originally creating the library
@@ -57,8 +79,8 @@ All help in providing PRs to close out bug issues is appreciated. Even if that i
- Bugfix: Fix test suite by switching to mockbin (thanks @jairhenrique)
- 4.0.2
- Fix mock imports as reported in #504 by @llybin. Thank you.
- 4.0.1
- Fix logo alignment for PyPI
- 4.0.1
- Fix logo alignment for PyPI
- 4.0.0
- Remove Python2 support (@hugovk)
- Add Python 3.8 TravisCI support (@neozenith)
@@ -70,7 +92,7 @@ All help in providing PRs to close out bug issues is appreciated. Even if that i
- Add support for `request_info` on mocked responses in aiohttp stub #495 (@nickdirienzo)
- doc: fixed variable name (a -> cass) in an example for rewind #492 (@yarikoptic)
- 2.1.1
- 2.1.1
- Format code with black (@neozenith)
- Use latest pypy3 in Travis (@hugovk)
- Improve documentation about custom matchers (@gward)
@@ -78,7 +100,7 @@ All help in providing PRs to close out bug issues is appreciated. Even if that i
- Add `pytest-recording` to the documentation as an alternative Pytest plugin (@Stranger6667)
- Fix yarl and python3.5 version issue (@neozenith)
- Fix header matcher for boto3 - fixes #474 (@simahawk)
- 2.1.0
- 2.1.0
- Add a `rewind` method to reset a cassette (thanks @khamidou)
- New error message with more details on why the cassette failed to play a request (thanks @arthurHamon2, @neozenith)
- Handle connect tunnel URI (thanks @jeking3)
@@ -90,9 +112,9 @@ All help in providing PRs to close out bug issues is appreciated. Even if that i
- Fix bugs on aiohttp integration (thanks @graingert, @steinnes, @stj, @lamenezes, @lmazuel)
- Fix Biopython incompatibility (thanks @rishab121)
- Fix Boto3 integration (thanks @1oglop1, @arthurHamon2)
- 2.0.1
- 2.0.1
- Fix bug when using vcrpy with python 3.4
- 2.0.0
- 2.0.0
- Support python 3.7 (fix httplib2 and urllib2, thanks @felixonmars)
- [#356] Fixes `before_record_response` so the original response isn't changed (thanks @kgraves)
- Fix requests stub when using proxy (thanks @samuelfekete @daneoshiga)
@@ -102,56 +124,56 @@ All help in providing PRs to close out bug issues is appreciated. Even if that i
- Improve docs (thanks @adamchainz)
- 1.13.0
- 1.13.0
- Fix support to latest aiohttp version (3.3.2). Fix content-type bug in aiohttp stub. Save URL with query params properly when using aiohttp.
- 1.12.0
- 1.12.0
- Fix support to latest aiohttp version (3.2.1), Adapted setup to PEP508, Support binary responses on aiohttp, Dropped support for EOL python versions (2.6 and 3.3)
- 1.11.1
- 1.11.1
- Fix compatibility with newest requests and urllib3 releases
- 1.11.0
- 1.11.0
- Allow injection of persistence methods + bugfixes (thanks @j-funk and @IvanMalison),
- Support python 3.6 + CI tests (thanks @derekbekoe and @graingert),
- Support pytest-asyncio coroutines (thanks @graingert)
- 1.10.5
- 1.10.5
- Added a fix to httplib2 (thanks @carlosds730), Fix an issue with
- aiohttp (thanks @madninja), Add missing requirement yarl (thanks @lamenezes),
- Remove duplicate mock triple (thanks @FooBarQuaxx)
- 1.10.4
- 1.10.4
- Fix an issue with asyncio aiohttp (thanks @madninja)
- 1.10.3
- 1.10.3
- Fix some issues with asyncio and params (thanks @anovikov1984 and @lamenezes)
- Fix some issues with cassette serialize / deserialize and empty response bodies (thanks @gRoussac and @dz0ny)
- 1.10.2
- 1.10.2
- Fix 1.10.1 release - add aiohttp support back in
- 1.10.1
- 1.10.1
- [bad release] Fix build for Fedora package + python2 (thanks @puiterwijk and @lamenezes)
- 1.10.0
- 1.10.0
- Add support for aiohttp (thanks @lamenezes)
- 1.9.0
- Add support for boto3 (thanks @desdm, @foorbarna).
- 1.9.0
- Add support for boto3 (thanks @desdm, @foorbarna).
- Fix deepcopy issue for response headers when `decode_compressed_response` is enabled (thanks @nickdirienzo)
- 1.8.0
- Fix for Serialization errors with JSON adapter (thanks @aliaksandrb).
- Avoid concatenating bytes with strings (thanks @jaysonsantos).
- Exclude __pycache__ dirs & compiled files in sdist (thanks @koobs).
- Fix Tornado support behavior for Tornado 3 (thanks @abhinav).
- 1.8.0
- Fix for Serialization errors with JSON adapter (thanks @aliaksandrb).
- Avoid concatenating bytes with strings (thanks @jaysonsantos).
- Exclude __pycache__ dirs & compiled files in sdist (thanks @koobs).
- Fix Tornado support behavior for Tornado 3 (thanks @abhinav).
- decode_compressed_response option and filter (thanks @jayvdb).
- 1.7.4 [#217]
- Make use_cassette decorated functions actually return a value (thanks @bcen).
- 1.7.4 [#217]
- Make use_cassette decorated functions actually return a value (thanks @bcen).
- [#199] Fix path transformation defaults.
- Better headers dictionary management.
- 1.7.3 [#188]
- 1.7.3 [#188]
- ``additional_matchers`` kwarg on ``use_cassette``.
- [#191] Actually support passing multiple before_record_request functions (thanks @agriffis).
- 1.7.2
- 1.7.2
- [#186] Get effective_url in tornado (thanks @mvschaik)
- [#187] Set request_time on Response object in tornado (thanks @abhinav).
- 1.7.1
- 1.7.1
- [#183] Patch ``fetch_impl`` instead of the entire HTTPClient class for Tornado (thanks @abhinav).
- 1.7.0
- [#177] Properly support coroutine/generator decoration.
- 1.7.0
- [#177] Properly support coroutine/generator decoration.
- [#178] Support distribute (thanks @graingert). [#163] Make compatibility between python2 and python3 recorded cassettes more robust (thanks @gward).
- 1.6.1
- 1.6.1
- [#169] Support conditional requirements in old versions of pip
- Fix RST parse errors generated by pandoc
- [Tornado] Fix unsupported features exception not being raised
@@ -165,17 +187,17 @@ All help in providing PRs to close out bug issues is appreciated. Even if that i
- Fix crash when cassette path contains cassette library directory (thanks @gazpachoking).
- 1.5.0
- Automatic cassette naming and 'application/json' post data filtering (thanks @marco-santamaria).
- 1.4.2
- 1.4.2
- Fix a bug caused by requests 2.7 and chunked transfer encoding
- 1.4.1
- 1.4.1
- Include README, tests, LICENSE in package. Thanks @ralphbean.
- 1.4.0
- 1.4.0
- Filter post data parameters (thanks @eadmundo)
- Support for posting files through requests, inject\_cassette kwarg to access cassette from ``use_cassette`` decorated function, ``with_current_defaults`` actually works (thanks @samstav).
- 1.3.0
- Fix/add support for urllib3 (thanks @aisch)
- Fix default port for https (thanks @abhinav).
- 1.2.0
- 1.2.0
- Add custom\_patches argument to VCR/Cassette objects to allow users to stub custom classes when cassettes become active.
- 1.1.4
- Add force reset around calls to actual connection from stubs, to ensure compatibility with the version of httplib/urlib2 in python 2.7.9.
@@ -186,22 +208,22 @@ All help in providing PRs to close out bug issues is appreciated. Even if that i
- fix Windows connectionpool stub bug (thanks @gazpachoking)
- add support for requests 2.5
- 1.1.2
- Add urllib==1.7.1 support.
- Add urllib==1.7.1 support.
- Make json serialize error handling correct
- Improve logging of match failures.
- 1.1.1
- Use function signature preserving ``wrapt.decorator`` to write the decorator version of use\_cassette in order to ensure compatibility with py.test fixtures and python 2.
- 1.1.1
- Use function signature preserving ``wrapt.decorator`` to write the decorator version of use\_cassette in order to ensure compatibility with py.test fixtures and python 2.
- Move all request filtering into the ``before_record_callable``.
- 1.1.0
- 1.1.0
- Add ``before_record_response``. Fix several bugs related to the context management of cassettes.
- 1.0.3
- 1.0.3
- Fix an issue with requests 2.4 and make sure case sensitivity is consistent across python versions
- 1.0.2
- Fix an issue with requests 2.3
- 1.0.1
- Fix a bug with the new ignore requests feature and the once record mode
- 1.0.0
- *BACKWARDS INCOMPATIBLE*: Please see the 'upgrade' section in the README. Take a look at the matcher section as well, you might want to update your ``match_on`` settings.
- 1.0.0
- *BACKWARDS INCOMPATIBLE*: Please see the 'upgrade' section in the README. Take a look at the matcher section as well, you might want to update your ``match_on`` settings.
- Add support for filtering sensitive data from requests, matching query strings after the order changes and improving the built-in matchers, (thanks to @mshytikov)
- Support for ignoring requests to certain hosts, bump supported Python3 version to 3.4, fix some bugs with Boto support (thanks @marusich)
- Fix error with URL field capitalization in README (thanks @simon-weber)
@@ -209,27 +231,27 @@ All help in providing PRs to close out bug issues is appreciated. Even if that i
- Added ``all_played`` property on cassette (thanks @mshytikov)
- 0.7.0
- VCR.py now supports Python 3! (thanks @asundg)
- VCR.py now supports Python 3! (thanks @asundg)
- Also I refactored the stub connections quite a bit to add support for the putrequest and putheader calls.
- This version also adds support for httplib2 (thanks @nilp0inter).
- This version also adds support for httplib2 (thanks @nilp0inter).
- I have added a couple tests for boto since it is an http client in its own right.
- Finally, this version includes a fix for a bug where requests wasn't being patched properly (thanks @msabramo).
- 0.6.0
- Store response headers as a list since a HTTP response can have the same header twice (happens with set-cookie sometimes).
- This has the added benefit of preserving the order of headers.
- Thanks @smallcode for the bug report leading to this change.
- This has the added benefit of preserving the order of headers.
- Thanks @smallcode for the bug report leading to this change.
- I have made an effort to ensure backwards compatibility with the old cassettes' header storage mechanism, but if you want to upgrade to the new header storage, you should delete your cassettes and re-record them.
- Also this release adds better error messages (thanks @msabramo)
- Also this release adds better error messages (thanks @msabramo)
- and adds support for using VCR as a decorator (thanks @smallcode for the motivation)
- 0.5.0
- Change the ``response_of`` method to ``responses_of`` since cassettes can now contain more than one response for a request.
- Since this changes the API, I'm bumping the version.
- Also includes 2 bugfixes:
- a better error message when attempting to overwrite a cassette file,
- Change the ``response_of`` method to ``responses_of`` since cassettes can now contain more than one response for a request.
- Since this changes the API, I'm bumping the version.
- Also includes 2 bugfixes:
- a better error message when attempting to overwrite a cassette file,
- and a fix for a bug with requests sessions (thanks @msabramo)
- 0.4.0
- Change default request recording behavior for multiple requests.
- If you make the same request multiple times to the same URL, the response might be different each time (maybe the response has a timestamp in it or something), so this will make the same request multiple times and save them all.
- If you make the same request multiple times to the same URL, the response might be different each time (maybe the response has a timestamp in it or something), so this will make the same request multiple times and save them all.
- Then, when you are replaying the cassette, the responses will be played back in the same order in which they were received.
- If you were making multiple requests to the same URL in a cassette before version 0.4.0, you might need to regenerate your cassette files.
- Also, removes support for the cassette.play\_count counter API, since individual requests aren't unique anymore.
@@ -249,7 +271,7 @@ All help in providing PRs to close out bug issues is appreciated. Even if that i
- 0.3.0
- *Backwards incompatible release*
- Added support for record modes, and changed the default recording behavior to the "once" record mode. Please see the documentation on record modes for more.
- Added support for custom request matching, and changed the default request matching behavior to match only on the URL and method.
- Added support for custom request matching, and changed the default request matching behavior to match only on the URL and method.
- Also, improved the httplib mocking to add support for the ``HTTPConnection.send()`` method.
- This means that requests won't actually be sent until the response is read, since I need to record the entire request in order to match up the appropriate response.
- I don't think this should cause any issues unless you are sending requests without ever loading the response (which none of the standard httplib wrappers do, as far as I know).
@@ -257,13 +279,13 @@ All help in providing PRs to close out bug issues is appreciated. Even if that i
- 0.2.1
- Fixed missing modules in setup.py
- 0.2.0
- Added configuration API, which lets you configure some settings on VCR (see the README).
- Added configuration API, which lets you configure some settings on VCR (see the README).
- Also, VCR no longer saves cassettes if they haven't changed at all and supports JSON as well as YAML (thanks @sirpengi).
- Added amazing new skeumorphic logo, thanks @hairarrow.
- 0.1.0
- *backwards incompatible release - delete your old cassette files*
- This release adds the ability to access the cassette to make assertions on it
- as well as a major code refactor thanks to @dlecocq.
- as well as a major code refactor thanks to @dlecocq.
- It also fixes a couple longstanding bugs with redirects and HTTPS. [#3 and #4]
- 0.0.4
- If you have libyaml installed, vcrpy will use the c bindings instead. Speed up your tests! Thanks @dlecocq
@@ -273,4 +295,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

View File

@@ -24,7 +24,7 @@ So whilst reporting issues are valuable, please consider:
- contributing an issue with a toy repo that replicates the issue.
- contributing PRs is a more valuable donation of your time and effort.
Thanks again for your interest and support in VCRpy.
Thanks again for your interest and support in VCRpy.
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``
- If it is a bug, is it ``Verified Can Replicate`` or ``Requires Help Replicating``
- Thanking people for raising issues. Feedback is always appreciated.
- Politely asking if they are able to link to an example repo that replicates the issue if they haven't already. Being able to *clone and go* helps the next person and we like that. 😃
- 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:**
@@ -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:
- Are there tests covering the code paths added/modified?
- Do the tests and modifications make sense seem appropriate?
- Add specific feedback, even on approvals, why it is accepted. eg "I like how you use a context manager there. 😄 "
- Add specific feedback, even on approvals, why it is accepted. eg "I like how you use a context manager there. 😄 "
- Also make sure they add a line to `docs/changelog.rst` to claim credit for their contribution.
**Release Manager:**
@@ -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.
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/>`_
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>`_
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 .[test]
./run_tests.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

View File

@@ -14,11 +14,10 @@ VCR.py supports Python 3.8+, and `pypy <http://pypy.org>`__.
The following HTTP libraries are supported:
- ``aiohttp``
- ``boto``
- ``boto3``
- ``http.client``
- ``httplib2``
- ``requests`` (both 1.x and 2.x versions)
- ``requests`` (>=2.16.2 versions)
- ``tornado.httpclient``
- ``urllib2``
- ``urllib3``

2
docs/requirements.txt Normal file
View File

@@ -0,0 +1,2 @@
sphinx<8
sphinx_rtd_theme==2.0.0

View File

@@ -1,13 +1,3 @@
[tool.black]
line-length=110
[tool.isort]
line_length = 110
known_first_party = "vcrpy"
multi_line_output = 3
use_parentheses = true
include_trailing_comma = true
[tool.codespell]
skip = '.git,*.pdf,*.svg,.tox'
ignore-regex = "\\\\[fnrstv]"
@@ -15,6 +5,32 @@ 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]
select = [
"B", # flake8-bugbear
"C4", # flake8-comprehensions
"COM", # flake8-commas
"E", # pycodestyle error
"F", # pyflakes
"I", # isort
"ISC", # flake8-implicit-str-concat
"PIE", # flake8-pie
"RUF", # Ruff-specific rules
"UP", # pyupgrade
"W", # pycodestyle warning
]
line-length = 110
target-version = "py38"
[tool.ruff.isort]
known-first-party = ["vcr"]

View File

@@ -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 "$@"

View File

@@ -45,35 +45,41 @@ class PyTest(TestCommand):
install_requires = [
"PyYAML",
"wrapt",
"six>=1.5",
"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'",
]
tests_require = [
"aiohttp",
"boto3",
"httplib2",
"httpx",
"pytest",
"pytest-aiohttp",
"pytest-httpbin",
"requests",
"tornado",
# Needed to un-break httpbin 0.7.0. For httpbin >=0.7.1 and after,
# this pin and the dependency itself can be removed, provided
# that the related bug in httpbin has been fixed:
# https://github.com/kevin1024/vcrpy/issues/645#issuecomment-1562489489
# https://github.com/postmanlabs/httpbin/issues/673
# https://github.com/postmanlabs/httpbin/pull/674
"Werkzeug==2.0.3",
]
extras_require = {
"tests": [
"aiohttp",
"boto3",
"httplib2",
"httpx",
"pytest-aiohttp",
"pytest-asyncio",
"pytest-cov",
"pytest-httpbin",
"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:
# https://github.com/kevin1024/vcrpy/issues/645#issuecomment-1562489489
# https://github.com/postmanlabs/httpbin/issues/673
# https://github.com/postmanlabs/httpbin/pull/674
"Werkzeug==2.0.3",
],
}
setup(
name="vcrpy",
@@ -88,7 +94,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",
@@ -99,6 +106,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
View File

View File

@@ -13,8 +13,10 @@ def assert_cassette_has_one_response(cass):
def assert_is_json_bytes(b: bytes):
assert isinstance(b, bytes)
try:
json.loads(b.decode("utf-8"))
except Exception:
assert False
json.loads(b)
except Exception as error:
raise AssertionError() from error
assert True

View File

@@ -15,9 +15,9 @@
},
"response": {
"status": {
"message": "OK",
"message": "OK",
"code": 200
},
},
"headers": {
"access-control-allow-origin": ["*"],
"content-type": ["application/json"],
@@ -25,7 +25,7 @@
"server": ["gunicorn/0.17.4"],
"content-length": ["32"],
"connection": ["keep-alive"]
},
},
"body": {
"string": "{\n \"origin\": \"217.122.164.194\"\n}"
}

View File

@@ -2,7 +2,7 @@ version: 1
interactions:
- request:
body: null
headers:
headers:
accept: ['*/*']
accept-encoding: ['gzip, deflate, compress']
user-agent: ['python-requests/2.2.1 CPython/2.6.1 Darwin/10.8.0']

View File

@@ -1,31 +1,31 @@
[
{
"request": {
"body": null,
"protocol": "http",
"method": "GET",
"body": null,
"protocol": "http",
"method": "GET",
"headers": {
"accept-encoding": "gzip, deflate, compress",
"accept": "*/*",
"accept-encoding": "gzip, deflate, compress",
"accept": "*/*",
"user-agent": "python-requests/2.2.1 CPython/2.6.1 Darwin/10.8.0"
},
"host": "httpbin.org",
"path": "/ip",
},
"host": "httpbin.org",
"path": "/ip",
"port": 80
},
},
"response": {
"status": {
"message": "OK",
"message": "OK",
"code": 200
},
},
"headers": [
"access-control-allow-origin: *\r\n",
"content-type: application/json\r\n",
"date: Mon, 21 Apr 2014 23:13:40 GMT\r\n",
"server: gunicorn/0.17.4\r\n",
"content-length: 32\r\n",
"access-control-allow-origin: *\r\n",
"content-type: application/json\r\n",
"date: Mon, 21 Apr 2014 23:13:40 GMT\r\n",
"server: gunicorn/0.17.4\r\n",
"content-length: 32\r\n",
"connection: keep-alive\r\n"
],
],
"body": {
"string": "{\n \"origin\": \"217.122.164.194\"\n}"
}

View File

@@ -10,7 +10,7 @@ interactions:
uri: http://seomoz.org/
response:
body: {string: ''}
headers:
headers:
Location: ['http://moz.com/']
Server: ['BigIP']
Connection: ['Keep-Alive']

View File

@@ -5,24 +5,24 @@ import aiohttp
async def aiohttp_request(loop, method, url, output="text", encoding="utf-8", content_type=None, **kwargs):
session = aiohttp.ClientSession(loop=loop)
response_ctx = session.request(method, url, **kwargs)
async with aiohttp.ClientSession(loop=loop) as session:
response_ctx = session.request(method, url, **kwargs)
response = await response_ctx.__aenter__()
if output == "text":
content = await response.text()
elif output == "json":
content_type = content_type or "application/json"
content = await response.json(encoding=encoding, content_type=content_type)
elif output == "raw":
content = await response.read()
elif output == "stream":
content = await response.content.read()
response = await response_ctx.__aenter__()
if output == "text":
content = await response.text()
elif output == "json":
content_type = content_type or "application/json"
content = await response.json(encoding=encoding, content_type=content_type)
elif output == "raw":
content = await response.read()
elif output == "stream":
content = await response.content.read()
response_ctx._resp.close()
await session.close()
response_ctx._resp.close()
await session.close()
return response, content
return response, content
def aiohttp_app():

View 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

View 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

View File

@@ -4,22 +4,6 @@ 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"]

View File

@@ -1,22 +1,22 @@
import contextlib
import logging
import urllib.parse
import pytest
import vcr
asyncio = pytest.importorskip("asyncio")
aiohttp = pytest.importorskip("aiohttp")
import vcr # noqa: E402
from .aiohttp_utils import aiohttp_app, aiohttp_request # noqa: E402
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):
@@ -35,8 +35,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)
@@ -49,8 +49,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)
@@ -59,15 +59,15 @@ def test_headers(tmpdir, auth, mockbin_request_url):
request = cassette.requests[0]
assert "AUTHORIZATION" in request.headers
cassette_response, _ = get(url, auth=auth)
assert dict(cassette_response.headers) == dict(response.headers)
assert cassette_response.headers.items() == response.headers.items()
assert cassette.play_count == 1
assert "istr" not in cassette.data[0]
assert "yarl.URL" not in cassette.data[0]
@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)
@@ -80,8 +80,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)
@@ -93,8 +93,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"))):
@@ -107,8 +107,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")
@@ -119,8 +119,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
@@ -133,10 +133,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})
@@ -158,14 +158,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)
@@ -174,8 +174,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"}
@@ -194,8 +194,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:
@@ -259,9 +259,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)
@@ -278,16 +284,14 @@ def test_redirect(tmpdir, mockbin):
# looking request_info.
assert cassette_response.request_info.url == response.request_info.url
assert cassette_response.request_info.method == response.request_info.method
assert {k: v for k, v in cassette_response.request_info.headers.items()} == {
k: v for k, v in response.request_info.headers.items()
}
assert cassette_response.request_info.headers.items() == response.request_info.headers.items()
assert cassette_response.request_info.real_url == response.request_info.real_url
@pytest.mark.online
def test_not_modified(tmpdir, mockbin):
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)
@@ -303,13 +307,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")
@@ -351,7 +355,10 @@ def test_cookies(httpbin_both, httpbin_ssl_context, tmpdir):
async with aiohttp.ClientSession(loop=loop, cookie_jar=aiohttp.CookieJar(unsafe=True)) as session:
cookies_resp = await session.get(cookies_url, ssl=httpbin_ssl_context)
home_resp = await session.get(
home_url, cookies=req_cookies, headers=req_headers, ssl=httpbin_ssl_context
home_url,
cookies=req_cookies,
headers=req_headers,
ssl=httpbin_ssl_context,
)
assert cassette.play_count == 0
assert_responses(cookies_resp, home_resp)
@@ -361,7 +368,10 @@ def test_cookies(httpbin_both, httpbin_ssl_context, tmpdir):
async with aiohttp.ClientSession(loop=loop, cookie_jar=aiohttp.CookieJar(unsafe=True)) as session:
cookies_resp = await session.get(cookies_url, ssl=httpbin_ssl_context)
home_resp = await session.get(
home_url, cookies=req_cookies, headers=req_headers, ssl=httpbin_ssl_context
home_url,
cookies=req_cookies,
headers=req_headers,
ssl=httpbin_ssl_context,
)
assert cassette.play_count == 2
assert_responses(cookies_resp, home_resp)
@@ -392,7 +402,8 @@ def test_cookies_redirect(httpbin_both, httpbin_ssl_context, tmpdir):
cookies = session.cookie_jar.filter_cookies(cookies_url)
assert cookies["Cookie_1"].value == "Val_1"
assert cassette.play_count == 0
cassette.requests[1].headers["Cookie"] == "Cookie_1=Val_1"
assert cassette.requests[1].headers["Cookie"] == "Cookie_1=Val_1"
# -------------------------- Play --------------------------- #
with vcr.use_cassette(tmp, record_mode=vcr.mode.NONE) as cassette:
@@ -402,12 +413,13 @@ def test_cookies_redirect(httpbin_both, httpbin_ssl_context, tmpdir):
cookies = session.cookie_jar.filter_cookies(cookies_url)
assert cookies["Cookie_1"].value == "Val_1"
assert cassette.play_count == 2
cassette.requests[1].headers["Cookie"] == "Cookie_1=Val_1"
assert cassette.requests[1].headers["Cookie"] == "Cookie_1=Val_1"
# Assert that it's ignoring expiration date
with vcr.use_cassette(tmp, record_mode=vcr.mode.NONE) as cassette:
cassette.responses[0]["headers"]["set-cookie"] = [
"Cookie_1=Val_1; Expires=Wed, 21 Oct 2015 07:28:00 GMT"
"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)
@@ -419,17 +431,33 @@ 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
def test_filter_query_parameters(tmpdir, httpbin):
url = httpbin + "?password=secret"
path = str(tmpdir.join("query_param_filter.yaml"))
with vcr.use_cassette(path, filter_query_parameters=["password"]) as cassette:
get(url)
assert "password" not in cassette.requests[0].url
assert "secret" not in cassette.requests[0].url
with open(path) as f:
cassette_content = f.read()
assert "password" not in cassette_content
assert "secret" not in cassette_content

View File

@@ -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"A simple HTTP Request &amp; Response Service." in response
def test_patched_content(tmpdir, httpbin):

View File

@@ -1,82 +0,0 @@
import pytest
boto = pytest.importorskip("boto")
from configparser import DuplicateSectionError # NOQA
import boto # NOQA
import boto.iam # NOQA
from boto.s3.connection import S3Connection # NOQA
from boto.s3.key import Key # NOQA
import vcr # 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()

View File

@@ -2,15 +2,14 @@ import os
import pytest
import vcr
boto3 = pytest.importorskip("boto3")
import boto3 # NOQA
import botocore # NOQA
import vcr # NOQA
import botocore # noqa
try:
from botocore import awsrequest # NOQA
from botocore import awsrequest # noqa
botocore_awsrequest = True
except ImportError:

View File

@@ -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")))

View File

@@ -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)

View File

@@ -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)
@@ -45,13 +46,18 @@ def test_filter_basic_auth(tmpdir, httpbin):
def test_filter_querystring(tmpdir, httpbin):
url = httpbin.url + "/?foo=bar"
url = httpbin.url + "/?password=secret"
cass_file = str(tmpdir.join("filter_qs.yaml"))
with vcr.use_cassette(cass_file, filter_query_parameters=["foo"]):
with vcr.use_cassette(cass_file, filter_query_parameters=["password"]):
urlopen(url)
with vcr.use_cassette(cass_file, filter_query_parameters=["foo"]) as cass:
with vcr.use_cassette(cass_file, filter_query_parameters=["password"]) as cass:
urlopen(url)
assert "foo" not in cass.requests[0].url
assert "password" not in cass.requests[0].url
assert "secret" not in cass.requests[0].url
with open(cass_file) as f:
cassette_content = f.read()
assert "password" not in cassette_content
assert "secret" not in cassette_content
def test_filter_post_data(tmpdir, httpbin):

View File

@@ -3,10 +3,11 @@ 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 +57,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)

View File

@@ -2,11 +2,18 @@ import os
import pytest
import vcr
from ..assertions import assert_is_json_bytes
asyncio = pytest.importorskip("asyncio")
httpx = pytest.importorskip("httpx")
import vcr # noqa: E402
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:
@@ -15,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)
@@ -24,21 +32,37 @@ 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):
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):
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):
@@ -65,8 +89,8 @@ class DoAsyncRequest(BaseDoRequest):
def client(self):
try:
return self._client
except AttributeError:
raise ValueError('To access async client, use "with do_request() as client"')
except AttributeError as e:
raise ValueError('To access async client, use "with do_request() as client"') from e
def __call__(self, *args, **kwargs):
if hasattr(self, "_loop"):
@@ -74,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):
@@ -88,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)
@@ -101,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)
@@ -115,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)
@@ -128,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"}
@@ -164,46 +201,26 @@ 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.
assert cassette_response.request.url == response.request.url
assert cassette_response.request.method == response.request.method
assert {k: v for k, v in cassette_response.request.headers.items()} == {
k: v for k, v in response.request.headers.items()
}
@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
assert cassette_response.request.headers.items() == response.request.headers.items()
@pytest.mark.online
@@ -218,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 [c for c in client.client.cookies]
return list(client.client.cookies)
def response_cookies(response):
return [c for c in response.cookies]
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:
@@ -278,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

View File

@@ -72,7 +72,12 @@ def test_method_matcher(cassette, httpbin, httpbin_secure):
@pytest.mark.parametrize(
"uri", [DEFAULT_URI, "http://httpbin.org/get?p2=q2&p1=q1", "http://httpbin.org/get?p2=q2&p1=q1"]
"uri",
(
DEFAULT_URI,
"http://httpbin.org/get?p2=q2&p1=q1",
"http://httpbin.org/get?p2=q2&p1=q1",
),
)
def test_default_matcher_matches(cassette, uri, httpbin, httpbin_secure):
uri = _replace_httpbin(uri, httpbin, httpbin_secure)

View File

@@ -3,6 +3,7 @@ from urllib.request import urlopen
import pytest
import vcr
from vcr.errors import CannotOverwriteExistingCassetteException
def test_making_extra_request_raises_exception(tmpdir, httpbin):
@@ -18,5 +19,5 @@ def test_making_extra_request_raises_exception(tmpdir, httpbin):
with vcr.use_cassette(str(tmpdir.join("test.json")), match_on=["method"]):
assert urlopen(httpbin.url + "/status/200").getcode() == 200
assert urlopen(httpbin.url + "/status/201").getcode() == 201
with pytest.raises(Exception):
with pytest.raises(CannotOverwriteExistingCassetteException):
urlopen(httpbin.url + "/status/200")

View File

@@ -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)
proxy_process.start()
yield "http://{}:{}".format(*httpd.server_address)
proxy_process.terminate()
with socketserver.ThreadingTCPServer(("", 0), Proxy) as httpd:
proxy_process = threading.Thread(target=httpd.serve_forever)
proxy_process.start()
yield "http://{}:{}".format(*httpd.server_address)
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

View File

@@ -3,6 +3,7 @@ from urllib.request import urlopen
import pytest
import vcr
from vcr.errors import CannotOverwriteExistingCassetteException
def test_once_record_mode(tmpdir, httpbin):
@@ -18,7 +19,7 @@ def test_once_record_mode(tmpdir, httpbin):
# the first time, it's played from the cassette.
# but, try to access something else from the same cassette, and an
# exception is raised.
with pytest.raises(Exception):
with pytest.raises(CannotOverwriteExistingCassetteException):
urlopen(httpbin.url + "/get").read()
@@ -94,7 +95,7 @@ def test_new_episodes_record_mode_two_times(tmpdir, httpbin):
assert urlopen(url).read() == original_second_response
# now that we are back in once mode, this should raise
# an error.
with pytest.raises(Exception):
with pytest.raises(CannotOverwriteExistingCassetteException):
urlopen(url).read()
@@ -124,7 +125,7 @@ def test_none_record_mode(tmpdir, httpbin):
# raise hell.
testfile = str(tmpdir.join("recordmode.yml"))
with vcr.use_cassette(testfile, record_mode=vcr.mode.NONE):
with pytest.raises(Exception):
with pytest.raises(CannotOverwriteExistingCassetteException):
urlopen(httpbin.url).read()
@@ -140,5 +141,5 @@ def test_none_record_mode_with_existing_cassette(tmpdir, httpbin):
urlopen(httpbin.url).read()
assert cass.play_count == 1
# but if I try to hit the net, raise an exception.
with pytest.raises(Exception):
with pytest.raises(CannotOverwriteExistingCassetteException):
urlopen(httpbin.url + "/get").read()

View File

@@ -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

View File

@@ -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"A simple HTTP Request &amp; Response Service." in response
def test_load_cassette_persister_exception_handling(tmpdir, httpbin):

View File

@@ -1,11 +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")
from requests.exceptions import ConnectionError # noqa E402
def test_status_code(httpbin_both, tmpdir):
@@ -171,7 +171,8 @@ def test_gzip__decode_compressed_response_true(tmpdir, httpbin_both):
assert expected_response.headers["content-encoding"] == "gzip" # self-test
with vcr.use_cassette(
str(tmpdir.join("decode_compressed.yaml")), decode_compressed_response=True
str(tmpdir.join("decode_compressed.yaml")),
decode_compressed_response=True,
) as cassette:
r = requests.get(url)
assert r.headers["content-encoding"] == "gzip" # i.e. not removed
@@ -264,7 +265,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 +273,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

View File

@@ -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)
@@ -120,8 +120,8 @@ def test_original_response_is_not_modified_by_before_filter(tmpdir, httpbin):
# The scrubbed field should be the same, because no cassette existed.
# Furthermore, the responses should be identical.
inside_body = json.loads(inside.read().decode("utf-8"))
outside_body = json.loads(outside.read().decode("utf-8"))
inside_body = json.loads(inside.read())
outside_body = json.loads(outside.read())
assert not inside_body[field_to_scrub] == replacement
assert inside_body[field_to_scrub] == outside_body[field_to_scrub]
@@ -131,5 +131,5 @@ def test_original_response_is_not_modified_by_before_filter(tmpdir, httpbin):
conn.request("GET", "/get")
inside = conn.getresponse()
inside_body = json.loads(inside.read().decode("utf-8"))
inside_body = json.loads(inside.read())
assert inside_body[field_to_scrub] == replacement

View File

@@ -1,19 +1,44 @@
"""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 +68,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 +81,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 +94,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 +108,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 +137,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 +161,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 +177,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 +189,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 +209,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 +235,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,13 +248,13 @@ 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."""
def callback(chunk):
assert False, "Did not expect to be called."
raise AssertionError("Did not expect to be called.")
with vcr.use_cassette(str(tmpdir.join("invalid.yaml"))):
future = get(get_client(), "http://httpbin.org", streaming_callback=callback)
@@ -232,23 +266,31 @@ 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."""
def callback(chunk):
assert False, "Did not expect to be called."
raise AssertionError("Did not expect to be called.")
with vcr.use_cassette(str(tmpdir.join("invalid.yaml"))):
response = yield get(
get_client(), "http://httpbin.org", streaming_callback=callback, raise_error=False
get_client(),
"http://httpbin.org",
streaming_callback=callback,
raise_error=False,
)
assert "not yet supported by VCR" in str(response.error)
@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."""
@@ -264,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.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."""
@@ -278,14 +324,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:
@@ -299,7 +345,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
@@ -312,7 +359,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."""
@@ -327,7 +375,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."""

View File

@@ -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()

View File

@@ -4,19 +4,21 @@
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")
@pytest.fixture(scope="module")
def verify_pool_mgr():
return urllib3.PoolManager(
cert_reqs="CERT_REQUIRED", ca_certs=pytest_httpbin.certs.where() # Force certificate check.
cert_reqs="CERT_REQUIRED",
ca_certs=pytest_httpbin.certs.where(), # Force certificate check.
)
@@ -98,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

View File

@@ -5,9 +5,9 @@ from xmlrpc.server import SimpleXMLRPCServer
import pytest
requests = pytest.importorskip("requests")
import vcr
import vcr # NOQA
requests = pytest.importorskip("requests")
def test_domain_redirect():
@@ -63,12 +63,12 @@ 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()
s.get(httpbin.url + "/cookies/set?k1=v1&k2=v2")
assert s.cookies.keys() == ["k1", "k2"]
with requests.Session() as s:
s.get(httpbin.url + "/cookies/set?k1=v1&k2=v2")
assert s.cookies.keys() == ["k1", "k2"]
r2 = s.get(httpbin.url + "/cookies")
assert sorted(r2.json()["cookies"].keys()) == ["k1", "k2"]
r2 = s.get(httpbin.url + "/cookies")
assert sorted(r2.json()["cookies"].keys()) == ["k1", "k2"]
@pytest.mark.online

View File

@@ -20,10 +20,13 @@ def test_cassette_load(tmpdir):
yaml.dump(
{
"interactions": [
{"request": {"body": "", "uri": "foo", "method": "GET", "headers": {}}, "response": "bar"}
]
}
)
{
"request": {"body": "", "uri": "foo", "method": "GET", "headers": {}},
"response": "bar",
},
],
},
),
)
a_cassette = Cassette.load(path=str(a_file))
assert len(a_cassette) == 1
@@ -110,7 +113,7 @@ def make_get_request():
@mock.patch("vcr.stubs.VCRHTTPResponse")
def test_function_decorated_with_use_cassette_can_be_invoked_multiple_times(*args):
decorated_function = Cassette.use(path="test")(make_get_request)
for i in range(4):
for _ in range(4):
decorated_function()
@@ -156,7 +159,7 @@ def test_cassette_allow_playback_repeats():
a = Cassette("test", allow_playback_repeats=True)
a.append("foo", "bar")
a.append("other", "resp")
for x in range(10):
for _ in range(10):
assert a.play_response("foo") == "bar"
assert a.play_count == 10
assert a.all_played is False
@@ -218,7 +221,7 @@ def test_nesting_cassette_context_managers(*args):
with contextlib.ExitStack() as exit_stack:
first_cassette = exit_stack.enter_context(Cassette.use(path="test"))
exit_stack.enter_context(
mock.patch.object(first_cassette, "play_response", return_value=first_response)
mock.patch.object(first_cassette, "play_response", return_value=first_response),
)
assert_get_response_body_is("first_response")

View File

@@ -55,15 +55,18 @@ from vcr.cassette import Cassette
],
)
def test_CannotOverwriteExistingCassetteException_get_message(
mock_find_requests_with_most_matches, most_matches, expected_message
mock_find_requests_with_most_matches,
most_matches,
expected_message,
):
mock_find_requests_with_most_matches.return_value = most_matches
cassette = Cassette("path")
failed_request = "request"
exception_message = errors.CannotOverwriteExistingCassetteException._get_message(cassette, "request")
expected = (
"Can't overwrite existing cassette (%r) in your current record mode (%r).\n"
"No match for the request (%r) was found.\n"
"%s" % (cassette._path, cassette.record_mode, failed_request, expected_message)
f"Can't overwrite existing cassette ({cassette._path!r}) "
f"in your current record mode ({cassette.record_mode!r}).\n"
f"No match for the request ({failed_request!r}) was found.\n"
f"{expected_message}"
)
assert exception_message == expected

View File

@@ -197,7 +197,7 @@ def test_replace_json_post_data_parameters():
("six", "doesntexist"),
],
)
request_data = json.loads(request.body.decode("utf-8"))
request_data = json.loads(request.body)
expected_data = json.loads('{"one": "keep", "three": "tada", "four": "SHOUT"}')
assert request_data == expected_data
@@ -208,8 +208,8 @@ def test_remove_json_post_data_parameters():
request = Request("POST", "http://google.com", body, {})
request.headers["Content-Type"] = "application/json"
remove_post_data_parameters(request, ["id"])
request_body_json = json.loads(request.body.decode("utf-8"))
expected_json = json.loads(b'{"foo": "bar", "baz": "qux"}'.decode("utf-8"))
request_body_json = json.loads(request.body)
expected_json = json.loads(b'{"foo": "bar", "baz": "qux"}')
assert request_body_json == expected_json

View File

@@ -63,6 +63,9 @@ boto3_bytes_headers = {
"Expect": b"100-continue",
"Content-Length": "21",
}
chunked_headers = {
"Transfer-Encoding": "chunked",
}
@pytest.mark.parametrize(
@@ -74,10 +77,16 @@ boto3_bytes_headers = {
),
(
request.Request(
"POST", "http://host.com/", "a=1&b=2", {"Content-Type": "application/x-www-form-urlencoded"}
"POST",
"http://host.com/",
"a=1&b=2",
{"Content-Type": "application/x-www-form-urlencoded"},
),
request.Request(
"POST", "http://host.com/", "b=2&a=1", {"Content-Type": "application/x-www-form-urlencoded"}
"POST",
"http://host.com/",
"b=2&a=1",
{"Content-Type": "application/x-www-form-urlencoded"},
),
),
(
@@ -86,23 +95,38 @@ boto3_bytes_headers = {
),
(
request.Request(
"POST", "http://host.com/", "a=1&b=2", {"Content-Type": "application/x-www-form-urlencoded"}
"POST",
"http://host.com/",
"a=1&b=2",
{"Content-Type": "application/x-www-form-urlencoded"},
),
request.Request(
"POST", "http://host.com/", "b=2&a=1", {"Content-Type": "application/x-www-form-urlencoded"}
"POST",
"http://host.com/",
"b=2&a=1",
{"Content-Type": "application/x-www-form-urlencoded"},
),
),
(
request.Request(
"POST", "http://host.com/", '{"a": 1, "b": 2}', {"Content-Type": "application/json"}
"POST",
"http://host.com/",
'{"a": 1, "b": 2}',
{"Content-Type": "application/json"},
),
request.Request(
"POST", "http://host.com/", '{"b": 2, "a": 1}', {"content-type": "application/json"}
"POST",
"http://host.com/",
'{"b": 2, "a": 1}',
{"content-type": "application/json"},
),
),
(
request.Request(
"POST", "http://host.com/", req1_body, {"User-Agent": "xmlrpclib", "Content-Type": "text/xml"}
"POST",
"http://host.com/",
req1_body,
{"User-Agent": "xmlrpclib", "Content-Type": "text/xml"},
),
request.Request(
"POST",
@@ -113,10 +137,16 @@ boto3_bytes_headers = {
),
(
request.Request(
"POST", "http://host.com/", '{"a": 1, "b": 2}', {"Content-Type": "application/json"}
"POST",
"http://host.com/",
'{"a": 1, "b": 2}',
{"Content-Type": "application/json"},
),
request.Request(
"POST", "http://host.com/", '{"b": 2, "a": 1}', {"content-type": "application/json"}
"POST",
"http://host.com/",
'{"b": 2, "a": 1}',
{"content-type": "application/json"},
),
),
(
@@ -124,6 +154,36 @@ boto3_bytes_headers = {
request.Request("POST", "http://aws.custom.com/", b"123", boto3_bytes_headers),
request.Request("POST", "http://aws.custom.com/", b"123", boto3_bytes_headers),
),
(
# chunked transfer encoding: decoded bytes versus encoded bytes
request.Request("POST", "scheme1://host1.test/", b"123456789_123456", chunked_headers),
request.Request(
"GET",
"scheme2://host2.test/",
b"10\r\n123456789_123456\r\n0\r\n\r\n",
chunked_headers,
),
),
(
# chunked transfer encoding: bytes iterator versus string iterator
request.Request(
"POST",
"scheme1://host1.test/",
iter([b"123456789_", b"123456"]),
chunked_headers,
),
request.Request("GET", "scheme2://host2.test/", iter(["123456789_", "123456"]), chunked_headers),
),
(
# chunked transfer encoding: bytes iterator versus single byte iterator
request.Request(
"POST",
"scheme1://host1.test/",
iter([b"123456789_", b"123456"]),
chunked_headers,
),
request.Request("GET", "scheme2://host2.test/", iter(b"123456789_123456"), chunked_headers),
),
],
)
def test_body_matcher_does_match(r1, r2):
@@ -139,10 +199,16 @@ def test_body_matcher_does_match(r1, r2):
),
(
request.Request(
"POST", "http://host.com/", '{"a": 1, "b": 3}', {"Content-Type": "application/json"}
"POST",
"http://host.com/",
'{"a": 1, "b": 3}',
{"Content-Type": "application/json"},
),
request.Request(
"POST", "http://host.com/", '{"b": 2, "a": 1}', {"content-type": "application/json"}
"POST",
"http://host.com/",
'{"b": 2, "a": 1}',
{"content-type": "application/json"},
),
),
(

View File

@@ -88,11 +88,11 @@ def test_response_parses_correctly_and_fp_attribute_error_is_not_thrown():
b"different types of cancer cells. Recently, the first HDACi was\n "
b"approved for the "
b"treatment of cutaneous T cell lymphomas. Most HDACi currently in\n "
b"clinical "
b"clinical ",
},
}
vcr_response = VCRHTTPResponse(recorded_response)
handle = io.TextIOWrapper(vcr_response, encoding="utf-8")
handle = iter(handle)
articles = [line for line in handle]
articles = list(handle)
assert len(articles) > 1

View File

@@ -1,3 +1,4 @@
import contextlib
from unittest import mock
from pytest import mark
@@ -16,7 +17,7 @@ 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")
vcr_connection.cassette = Cassette("test", record_mode=mode.ALL)
vcr_connection.real_connection.connect()
assert vcr_connection.real_connection.sock is not None
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

View File

@@ -100,9 +100,9 @@ def test_vcr_kwargs_cassette_dir():
pass
def _get_vcr_kwargs(self):
return dict(
record_mode="new_episodes",
)
return {
"record_mode": "new_episodes",
}
_get_cassette_library_dir = MagicMock(return_value="/testing")
@@ -117,9 +117,9 @@ def test_vcr_kwargs_cassette_dir():
pass
def _get_vcr_kwargs(self):
return dict(
cassette_library_dir="/testing",
)
return {
"cassette_library_dir": "/testing",
}
_get_cassette_library_dir = MagicMock(return_value="/ignored")

View File

@@ -15,7 +15,8 @@ def test_vcr_use_cassette():
record_mode = mock.Mock()
test_vcr = VCR(record_mode=record_mode)
with mock.patch(
"vcr.cassette.Cassette.load", return_value=mock.MagicMock(inject=False)
"vcr.cassette.Cassette.load",
return_value=mock.MagicMock(inject=False),
) as mock_cassette_load:
@test_vcr.use_cassette("test")
@@ -71,16 +72,19 @@ def test_vcr_before_record_request_params():
# Test filter_headers
request = Request(
"GET", base_path + "?foo=bar", "", {"cookie": "test", "other": "fun", "bert": "nobody"}
"GET",
base_path + "?foo=bar",
"",
{"cookie": "test", "other": "fun", "bert": "nobody"},
)
assert cassette.filter_request(request).headers == {"other": "fun", "bert": "ernie"}
# Test ignore_hosts
request = Request("GET", "http://www.test.com" + "?foo=bar", "", {"cookie": "test", "other": "fun"})
request = Request("GET", "http://www.test.com?foo=bar", "", {"cookie": "test", "other": "fun"})
assert cassette.filter_request(request) is None
# Test ignore_localhost
request = Request("GET", "http://localhost:8000" + "?foo=bar", "", {"cookie": "test", "other": "fun"})
request = Request("GET", "http://localhost:8000?foo=bar", "", {"cookie": "test", "other": "fun"})
assert cassette.filter_request(request) is None
with test_vcr.use_cassette("test", before_record_request=None) as cassette:
@@ -259,7 +263,9 @@ def test_cassette_library_dir_with_decoration_and_super_explicit_path():
def test_cassette_library_dir_with_path_transformer():
library_dir = "/library_dir"
vcr = VCR(
inject_cassette=True, cassette_library_dir=library_dir, path_transformer=lambda path: path + ".json"
inject_cassette=True,
cassette_library_dir=library_dir,
path_transformer=lambda path: path + ".json",
)
@vcr.use_cassette()
@@ -362,7 +368,7 @@ del test_dynamically_added
def test_path_class_as_cassette():
path = Path(__file__).parent.parent.joinpath(
"integration/cassettes/test_httpx_test_test_behind_proxy.yml"
"integration/cassettes/test_httpx_test_test_behind_proxy.yml",
)
with use_cassette(path):
pass

View File

@@ -8,8 +8,4 @@ def test_vcr_import_deprecation(recwarn):
import vcr # noqa: F401
if sys.version_info[0] == 2:
assert len(recwarn) == 1
assert issubclass(recwarn[0].category, DeprecationWarning)
else:
assert len(recwarn) == 0
assert len(recwarn) == 0

110
tox.ini
View File

@@ -1,110 +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 .
isort --version
isort . --check --diff
flake8 --version
flake8 --exclude=./docs/conf.py,./.tox/,./venv/
pyflakes ./docs/conf.py
deps =
flake8
black
isort
basepython = python3.10
[testenv:docs]
# Running sphinx from inside the "docs" directory
# ensures it will not pick up any stray files that might
# get into a virtual environment under the top-level directory
# or other artifacts under build/
changedir = docs
# The only dependency is sphinx
# If we were using extensions packaged separately,
# we would specify them here.
# A better practice is to specify a specific version of sphinx.
deps =
sphinx
sphinx_rtd_theme
# This is the sphinx command to generate HTML.
# In other circumstances, we might want to generate a PDF or an ebook
commands =
sphinx-build -W -b html -d {envtmpdir}/doctrees . {envtmpdir}/html
# We use Python 3.8. Tox sometimes tries to autodetect it based on the name of
# the testenv, but "docs" does not give useful clues so we have to be explicit.
basepython = python3.8
[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
[flake8]
max_line_length = 110

View File

@@ -2,9 +2,9 @@ import logging
from logging import NullHandler
from .config import VCR
from .record_mode import RecordMode as mode # noqa import is not used in this file
from .record_mode import RecordMode as mode # noqa: F401
__version__ = "5.0.0"
__version__ = "6.0.0"
logging.getLogger(__name__).addHandler(NullHandler())

View File

@@ -1,3 +1,3 @@
async def handle_coroutine(vcr, fn): # noqa: E999
async def handle_coroutine(vcr, fn):
with vcr as cassette:
return await fn(cassette) # noqa: E999
return await fn(cassette)

View File

@@ -3,7 +3,7 @@ import contextlib
import copy
import inspect
import logging
import sys
from asyncio import iscoroutinefunction
import wrapt
@@ -16,14 +16,6 @@ from .record_mode import RecordMode
from .serializers import yamlserializer
from .util import partition_dict
try:
from asyncio import iscoroutinefunction
except ImportError:
def iscoroutinefunction(*args, **kwargs):
return False
log = logging.getLogger(__name__)
@@ -81,7 +73,8 @@ class CassetteContextDecorator:
# pass
assert self.__finish is None, "Cassette already open."
other_kwargs, cassette_kwargs = partition_dict(
lambda key, _: key in self._non_cassette_arguments, self._args_getter()
lambda key, _: key in self._non_cassette_arguments,
self._args_getter(),
)
if other_kwargs.get("path_transformer"):
transformer = other_kwargs["path_transformer"]
@@ -132,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
yield from fn(cassette)
def _handle_function(self, fn):
with self as cassette:
@@ -280,7 +260,7 @@ class Cassette:
return response
# The cassette doesn't contain the request asked for.
raise UnhandledHTTPRequestError(
f"The cassette ({self._path!r}) doesn't contain the request ({request!r}) asked for"
f"The cassette ({self._path!r}) doesn't contain the request ({request!r}) asked for",
)
def responses_of(self, request):
@@ -295,7 +275,7 @@ class Cassette:
return responses
# The cassette doesn't contain the request asked for.
raise UnhandledHTTPRequestError(
f"The cassette ({self._path!r}) doesn't contain the request ({request!r}) asked for"
f"The cassette ({self._path!r}) doesn't contain the request ({request!r}) asked for",
)
def rewind(self):
@@ -314,7 +294,7 @@ class Cassette:
"""
best_matches = []
request = self._before_record_request(request)
for index, (stored_request, response) in enumerate(self.data):
for _, (stored_request, _) in enumerate(self.data):
successes, fails = get_matchers_results(request, stored_request, self._match_on)
best_matches.append((len(successes), stored_request, successes, fails))
best_matches.sort(key=lambda t: t[0], reverse=True)
@@ -364,7 +344,7 @@ class Cassette:
def __contains__(self, request):
"""Return whether or not a request has been stored"""
for index, response in self._responses(request):
for index, _ in self._responses(request):
if self.play_counts[index] == 0 or self.allow_playback_repeats:
return True
return False

View File

@@ -6,8 +6,6 @@ import types
from collections import abc as collections_abc
from pathlib import Path
import six
from . import filters, matchers
from .cassette import Cassette
from .persisters.filesystem import FilesystemPersister
@@ -88,7 +86,7 @@ class VCR:
try:
serializer = self.serializers[serializer_name]
except KeyError:
raise KeyError(f"Serializer {serializer_name} doesn't exist or isn't registered")
raise KeyError(f"Serializer {serializer_name} doesn't exist or isn't registered") from None
return serializer
def _get_matchers(self, matcher_names):
@@ -97,7 +95,7 @@ class VCR:
for m in matcher_names:
matchers.append(self.matchers[m])
except KeyError:
raise KeyError(f"Matcher {m} doesn't exist or isn't registered")
raise KeyError(f"Matcher {m} doesn't exist or isn't registered") from None
return matchers
def use_cassette(self, path=None, **kwargs):
@@ -162,7 +160,8 @@ class VCR:
def _build_before_record_response(self, options):
before_record_response = options.get("before_record_response", self.before_record_response)
decode_compressed_response = options.get(
"decode_compressed_response", self.decode_compressed_response
"decode_compressed_response",
self.decode_compressed_response,
)
filter_functions = []
if decode_compressed_response:
@@ -186,10 +185,12 @@ class VCR:
filter_headers = options.get("filter_headers", self.filter_headers)
filter_query_parameters = options.get("filter_query_parameters", self.filter_query_parameters)
filter_post_data_parameters = options.get(
"filter_post_data_parameters", self.filter_post_data_parameters
"filter_post_data_parameters",
self.filter_post_data_parameters,
)
before_record_request = options.get(
"before_record_request", options.get("before_record", self.before_record_request)
"before_record_request",
options.get("before_record", self.before_record_request),
)
ignore_hosts = options.get("ignore_hosts", self.ignore_hosts)
ignore_localhost = options.get("ignore_localhost", self.ignore_localhost)
@@ -199,12 +200,12 @@ class VCR:
if filter_query_parameters:
replacements = [p if isinstance(p, tuple) else (p, None) for p in filter_query_parameters]
filter_functions.append(
functools.partial(filters.replace_query_parameters, replacements=replacements)
functools.partial(filters.replace_query_parameters, replacements=replacements),
)
if filter_post_data_parameters:
replacements = [p if isinstance(p, tuple) else (p, None) for p in filter_post_data_parameters]
filter_functions.append(
functools.partial(filters.replace_post_data_parameters, replacements=replacements)
functools.partial(filters.replace_post_data_parameters, replacements=replacements),
)
hosts_to_ignore = set(ignore_hosts)
@@ -253,5 +254,5 @@ class VCR:
def test_case(self, predicate=None):
predicate = predicate or self.is_test_method
# TODO: Remove this reference to `six` in favor of the Python3 equivalent
return six.with_metaclass(auto_decorate(self.use_cassette, predicate))
metaclass = auto_decorate(self.use_cassette, predicate)
return metaclass("temporary_class", (), {})

View File

@@ -13,30 +13,29 @@ class CannotOverwriteExistingCassetteException(Exception):
best_matches = cassette.find_requests_with_most_matches(failed_request)
if best_matches:
# Build a comprehensible message to put in the exception.
best_matches_msg = "Found {} similar requests with {} different matcher(s) :\n".format(
len(best_matches), len(best_matches[0][2])
best_matches_msg = (
f"Found {len(best_matches)} similar requests "
f"with {len(best_matches[0][2])} different matcher(s) :\n"
)
for idx, best_match in enumerate(best_matches, start=1):
request, succeeded_matchers, failed_matchers_assertion_msgs = best_match
best_matches_msg += (
"\n%s - (%r).\n"
"Matchers succeeded : %s\n"
"Matchers failed :\n" % (idx, request, succeeded_matchers)
f"\n{idx} - ({request!r}).\n"
f"Matchers succeeded : {succeeded_matchers}\n"
"Matchers failed :\n"
)
for failed_matcher, assertion_msg in failed_matchers_assertion_msgs:
best_matches_msg += "%s - assertion failure :\n" "%s\n" % (failed_matcher, assertion_msg)
best_matches_msg += f"{failed_matcher} - assertion failure :\n{assertion_msg}\n"
else:
best_matches_msg = "No similar requests, that have not been played, found."
return (
"Can't overwrite existing cassette (%r) in "
"your current record mode (%r).\n"
"No match for the request (%r) was found.\n"
"%s" % (cassette._path, cassette.record_mode, failed_request, best_matches_msg)
f"Can't overwrite existing cassette ({cassette._path!r}) in "
f"your current record mode ({cassette.record_mode!r}).\n"
f"No match for the request ({failed_request!r}) was found.\n"
f"{best_matches_msg}"
)
class UnhandledHTTPRequestError(KeyError):
"""Raised when a cassette does not contain the request we want."""
pass

View File

@@ -95,7 +95,7 @@ def replace_post_data_parameters(request, replacements):
new_body[k] = rv
request.body = new_body
elif request.headers.get("Content-Type") == "application/json":
json_data = json.loads(request.body.decode("utf-8"))
json_data = json.loads(request.body)
for k, rv in replacements.items():
if k in json_data:
ov = json_data.pop(k)

View File

@@ -2,9 +2,13 @@ import json
import logging
import urllib
import xmlrpc.client
from string import hexdigits
from typing import List, Set
from .util import read_body
_HEXDIG_CODE_POINTS: Set[int] = {ord(s.encode("ascii")) for s in hexdigits}
log = logging.getLogger(__name__)
@@ -49,11 +53,17 @@ def raw_body(r1, r2):
def body(r1, r2):
transformer = _get_transformer(r1)
r2_transformer = _get_transformer(r2)
if transformer != r2_transformer:
transformer = _identity
if transformer(read_body(r1)) != transformer(read_body(r2)):
transformers = list(_get_transformers(r1))
if transformers != list(_get_transformers(r2)):
transformers = []
b1 = read_body(r1)
b2 = read_body(r2)
for transform in transformers:
b1 = transform(b1)
b2 = transform(b2)
if b1 != b2:
raise AssertionError
@@ -72,17 +82,71 @@ def _header_checker(value, header="Content-Type"):
return checker
def _dechunk(body):
if isinstance(body, str):
body = body.encode("utf-8")
elif isinstance(body, bytearray):
body = bytes(body)
elif hasattr(body, "__iter__"):
body = list(body)
if body:
if isinstance(body[0], str):
body = ("".join(body)).encode("utf-8")
elif isinstance(body[0], bytes):
body = b"".join(body)
elif isinstance(body[0], int):
body = bytes(body)
else:
raise ValueError(f"Body chunk type {type(body[0])} not supported")
else:
body = None
if not isinstance(body, bytes):
return body
# Now decode chunked data format (https://en.wikipedia.org/wiki/Chunked_transfer_encoding)
# Example input: b"45\r\n<69 bytes>\r\n0\r\n\r\n" where int(b"45", 16) == 69.
CHUNK_GAP = b"\r\n"
BODY_LEN: int = len(body)
chunks: List[bytes] = []
pos: int = 0
while True:
for i in range(pos, BODY_LEN):
if body[i] not in _HEXDIG_CODE_POINTS:
break
if i == 0 or body[i : i + len(CHUNK_GAP)] != CHUNK_GAP:
if pos == 0:
return body # i.e. assume non-chunk data
raise ValueError("Malformed chunked data")
size_bytes = int(body[pos:i], 16)
if size_bytes == 0: # i.e. well-formed ending
return b"".join(chunks)
chunk_data_first = i + len(CHUNK_GAP)
chunk_data_after_last = chunk_data_first + size_bytes
if body[chunk_data_after_last : chunk_data_after_last + len(CHUNK_GAP)] != CHUNK_GAP:
raise ValueError("Malformed chunked data")
chunk_data = body[chunk_data_first:chunk_data_after_last]
chunks.append(chunk_data)
pos = chunk_data_after_last + len(CHUNK_GAP)
def _transform_json(body):
# Request body is always a byte string, but json.loads() wants a text
# string. RFC 7159 says the default encoding is UTF-8 (although UTF-16
# and UTF-32 are also allowed: hmmmmm).
if body:
return json.loads(body.decode("utf-8"))
return json.loads(body)
_xml_header_checker = _header_checker("text/xml")
_xmlrpc_header_checker = _header_checker("xmlrpc", header="User-Agent")
_checker_transformer_pairs = (
(_header_checker("chunked", header="Transfer-Encoding"), _dechunk),
(
_header_checker("application/x-www-form-urlencoded"),
lambda body: urllib.parse.parse_qs(body.decode("ascii")),
@@ -92,16 +156,10 @@ _checker_transformer_pairs = (
)
def _identity(x):
return x
def _get_transformer(request):
def _get_transformers(request):
for checker, transformer in _checker_transformer_pairs:
if checker(request.headers):
return transformer
else:
return _identity
yield transformer
def requests_match(r1, r2, matchers):

View File

@@ -92,7 +92,7 @@ def migrate_json(in_fp, out_fp):
def _list_of_tuples_to_dict(fs):
return {k: v for k, v in fs[0]}
return dict(fs[0])
def _already_migrated(data):
@@ -130,7 +130,7 @@ def migrate(file_path, migration_fn):
def try_migrate(path):
if path.endswith(".json"):
return migrate(path, migrate_json)
elif path.endswith(".yaml") or path.endswith(".yml"):
elif path.endswith((".yaml", ".yml")):
return migrate(path, migrate_yml)
return False
@@ -138,7 +138,7 @@ def try_migrate(path):
def main():
if len(sys.argv) != 2:
raise SystemExit(
"Please provide path to cassettes directory or file. " "Usage: python3 -m vcr.migration PATH"
"Please provide path to cassettes directory or file. Usage: python3 -m vcr.migration PATH",
)
path = sys.argv[1]

View File

@@ -24,7 +24,7 @@ except ImportError as e:
else:
raise RuntimeError(
"vcrpy >=4.2.2 and botocore <1.11.0 are not compatible"
"; please upgrade botocore (or downgrade vcrpy)"
"; please upgrade botocore (or downgrade vcrpy)",
) from e
else:
_Boto3VerifiedHTTPSConnection = AWSHTTPSConnection
@@ -53,7 +53,7 @@ else:
if requests.__build__ < 0x021602:
raise RuntimeError(
"vcrpy >=4.2.2 and requests <2.16.2 are not compatible"
"; please upgrade requests (or downgrade vcrpy)"
"; please upgrade requests (or downgrade vcrpy)",
)
@@ -67,14 +67,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 +95,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 +118,6 @@ class CassettePatcherBuilder:
self._boto3(),
self._urllib3(),
self._httplib2(),
self._boto(),
self._tornado(),
self._aiohttp(),
self._httpx(),
@@ -144,7 +135,9 @@ class CassettePatcherBuilder:
return
return mock.patch.object(
obj, patched_attribute, self._recursively_apply_get_cassette_subclass(replacement_class)
obj,
patched_attribute,
self._recursively_apply_get_cassette_subclass(replacement_class),
)
def _recursively_apply_get_cassette_subclass(self, replacement_dict_or_obj):
@@ -186,7 +179,7 @@ class CassettePatcherBuilder:
bases = (base_class,)
if not issubclass(base_class, object): # Check for old style class
bases += (object,)
return type(f"{base_class.__name__}{self._cassette._path}", bases, dict(cassette=self._cassette))
return type(f"{base_class.__name__}{self._cassette._path}", bases, {"cassette": self._cassette})
@_build_patchers_from_mock_triples_decorator
def _httplib(self):
@@ -267,21 +260,14 @@ class CassettePatcherBuilder:
yield cpool, "HTTPConnectionWithTimeout", VCRHTTPConnectionWithTimeout
yield cpool, "HTTPSConnectionWithTimeout", VCRHTTPSConnectionWithTimeout
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
yield (
cpool,
"SCHEME_TO_CONNECTION",
{
"http": VCRHTTPConnectionWithTimeout,
"https": VCRHTTPSConnectionWithTimeout,
},
)
@_build_patchers_from_mock_triples_decorator
def _tornado(self):
@@ -325,18 +311,18 @@ 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(
self._get_cassette_subclass(stubs.VCRRequestsHTTPConnection)
self._get_cassette_subclass(stubs.VCRRequestsHTTPConnection),
)
https_connection_remover = ConnectionRemover(
self._get_cassette_subclass(stubs.VCRRequestsHTTPSConnection)
self._get_cassette_subclass(stubs.VCRRequestsHTTPSConnection),
)
mock_triples = (
(conn, "VerifiedHTTPSConnection", stubs.VCRRequestsHTTPSConnection),
@@ -386,10 +372,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
@@ -400,10 +382,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():
@@ -445,13 +430,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

View File

@@ -46,8 +46,9 @@ class Request:
def add_header(self, key, value):
warnings.warn(
"Request.add_header is deprecated. " "Please assign to request.headers instead.",
"Request.add_header is deprecated. Please assign to request.headers instead.",
DeprecationWarning,
stacklevel=2,
)
self.headers[key] = value

View File

@@ -28,7 +28,7 @@ def _warn_about_old_cassette_format():
raise ValueError(
"Your cassette files were generated in an older version "
"of VCR. Delete your cassettes or run the migration script."
"See http://git.io/mHhLBg for more details."
"See http://git.io/mHhLBg for more details.",
)

View File

@@ -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:

View File

@@ -1,7 +1,4 @@
try:
import simplejson as json
except ImportError:
import json
import json
def deserialize(cassette_string):
@@ -17,13 +14,5 @@ def serialize(cassette_dict):
try:
return json.dumps(cassette_dict, indent=4) + "\n"
except UnicodeDecodeError as original: # py2
raise UnicodeDecodeError(
original.encoding,
b"Error serializing cassette to JSON",
original.start,
original.end,
original.args[-1] + error_message,
)
except TypeError: # py3
raise TypeError(error_message)
except TypeError:
raise TypeError(error_message) from None

View File

@@ -261,7 +261,8 @@ class VCRConnection:
else:
if self.cassette.write_protected and self.cassette.filter_request(self._vcr_request):
raise CannotOverwriteExistingCassetteException(
cassette=self.cassette, failed_request=self._vcr_request
cassette=self.cassette,
failed_request=self._vcr_request,
)
# Otherwise, we should send the request, then get the response
@@ -388,6 +389,8 @@ class VCRHTTPConnection(VCRConnection):
_baseclass = HTTPConnection
_protocol = "http"
debuglevel = _baseclass.debuglevel
_http_vsn = _baseclass._http_vsn
class VCRHTTPSConnection(VCRConnection):
@@ -396,3 +399,5 @@ class VCRHTTPSConnection(VCRConnection):
_baseclass = HTTPSConnection
_protocol = "https"
is_verified = True
debuglevel = _baseclass.debuglevel
_http_vsn = _baseclass._http_vsn

View File

@@ -35,7 +35,7 @@ class MockClientResponse(ClientResponse):
session=None,
)
async def json(self, *, encoding="utf-8", loads=json.loads, **kwargs): # NOQA: E999
async def json(self, *, encoding="utf-8", loads=json.loads, **kwargs):
stripped = self._body.strip()
if not stripped:
return None
@@ -66,7 +66,7 @@ def build_response(vcr_request, vcr_response, history):
headers=_deserialize_headers(vcr_request.headers),
real_url=URL(vcr_request.url),
)
response = MockClientResponse(vcr_request.method, URL(vcr_response.get("url")), request_info=request_info)
response = MockClientResponse(vcr_request.method, URL(vcr_request.url), request_info=request_info)
response.status = vcr_response["status"]["code"]
response._body = vcr_response["body"].get("string", b"")
response.reason = vcr_response["status"]["message"]
@@ -162,8 +162,7 @@ async def record_response(cassette, vcr_request, response):
vcr_response = {
"status": {"code": response.status, "message": response.reason},
"headers": _serialize_headers(response.headers),
"body": body, # NOQA: E999
"url": str(response.url),
"body": body,
}
cassette.append(vcr_request, vcr_response)
@@ -273,7 +272,7 @@ def vcr_request(cassette, real_request):
log.info("%s not in cassette, sending to real server", vcr_request)
response = await real_request(self, method, url, **kwargs) # NOQA: E999
response = await real_request(self, method, url, **kwargs)
await record_responses(cassette, vcr_request, response)
return response

View File

@@ -1,9 +0,0 @@
"""Stubs for boto"""
from boto.https_connection import CertValidatingHTTPSConnection
from ..stubs import VCRHTTPSConnection
class VCRCertValidatingHTTPSConnection(VCRHTTPSConnection):
_baseclass = CertValidatingHTTPSConnection

View File

@@ -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()
# 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(
status_code=serialized_response.get("status_code"),
status_code=serialized_response["status"]["code"],
request=request,
headers=_from_serialized_headers(serialized_response.get("headers")),
content=content,
headers=_from_serialized_headers(serialized_response["headers"]),
content=serialized_response["body"]["string"],
history=history or [],
extensions=extensions,
)
response._content = content
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):

View File

@@ -29,9 +29,9 @@ def vcr_fetch_impl(cassette, real_fetch_impl):
request,
599,
error=Exception(
"The request (%s) uses AsyncHTTPClient functionality "
f"The request ({request!r}) uses AsyncHTTPClient functionality "
"that is not yet supported by VCR.py. Please make the "
"request outside a VCR.py context." % repr(request)
"request outside a VCR.py context.",
),
request_time=self.io_loop.time() - request.start_time,
)
@@ -65,7 +65,8 @@ def vcr_fetch_impl(cassette, real_fetch_impl):
request,
599,
error=CannotOverwriteExistingCassetteException(
cassette=cassette, failed_request=vcr_request
cassette=cassette,
failed_request=vcr_request,
),
request_time=self.io_loop.time() - request.start_time,
)

View File

@@ -27,7 +27,7 @@ class CaseInsensitiveDict(MutableMapping):
"""
def __init__(self, data=None, **kwargs):
self._store = dict()
self._store = {}
if data is None:
data = {}
self.update(data, **kwargs)