1
0
mirror of https://github.com/kevin1024/vcrpy.git synced 2025-12-09 01:03:24 +00:00

Compare commits

..

119 Commits

Author SHA1 Message Date
Kevin McCarthy
62cb151918 Release v4.3.0 2023-05-24 13:48:31 -05:00
Sebastian Pipping
1a3bc67c7c Merge pull request #701 from kevin1024/run-actions-on-push-to-topic-branches
Allow triggering CI manually
2023-05-17 18:01:28 +02:00
Sebastian Pipping
aeff51bd79 main.yml: Allow triggering CI manually 2023-05-17 16:45:13 +02:00
Sebastian Pipping
e9f0ede9dd main.yml: Drop superflous specification of branches 2023-05-17 16:44:54 +02:00
Sebastian Pipping
0235eab766 Merge pull request #698 from kevin1024/reduce-legacy
Drop support for botocore <1.11.0 and requests <2.16.2 (fixes #693)
2023-05-15 15:55:15 +02:00
Sebastian Pipping
31c8dc0a1e Drop support for requests <2.16.2 2023-05-15 14:06:26 +02:00
Sebastian Pipping
24af48d468 Drop support for botocore <1.11.0 2023-05-15 14:06:26 +02:00
Kian-Meng Ang
44359bfe43 Fix typo, succeedes -> succeeds (#672)
Found via `codespell`.
2023-05-13 17:03:27 +02:00
Jair Henrique
14cef83c15 Move some tests to use mockbin instead httpbin 2023-05-11 17:03:54 -03:00
Jair Henrique
77da67ef0a Remove duplicated fixture 2023-05-11 10:05:50 -03:00
dependabot[bot]
58329f812b build(deps): bump actions/checkout from 3.1.0 to 3.5.2
Bumps [actions/checkout](https://github.com/actions/checkout) from 3.1.0 to 3.5.2.
- [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.1.0...v3.5.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-05-08 09:15:50 -03:00
Sebastian Pipping
06913ce21a tox.ini: Exclude ./venv/ from flake8 2023-05-05 10:14:46 -03:00
Sebastian Pipping
4994c53590 Fix formatting with regard to black 23.3.0 2023-05-05 10:14:46 -03:00
Sebastian Pipping
1d90853f3b tox.ini: Quick-fix the CI for recent tox and recent urllib3 2023-05-05 10:14:46 -03:00
Terseus
42d79b1102 Restore the pytest-httpbin package in tox.ini
The branch with the fix for HTTPS redirects is included in v1.0.1

See https://github.com/kevin1024/pytest-httpbin/releases/tag/v1.0.1
2022-11-01 09:08:59 -03:00
Jair Henrique
cef85a4986 remove pytest deprecation warning for yield_fixture 2022-10-31 22:44:26 -03:00
Terseus
964615af25 Include how to use record_on_exception in the docs 2022-10-31 22:43:03 -03:00
Terseus
3b6d79fc0b Prevent Sphinx warning about language = None
Since version 5.x Sphinx triggers a warning when `language = None`, this
caused `tox -e docs` to fail.
Set it to "en".

See https://github.com/sphinx-doc/sphinx/pull/10481
2022-10-31 22:43:03 -03:00
Terseus
f48922ce09 Fix not calling all the exit stack when record_on_exception is False
The initial technique to implement `record_on_exception=False` was to
not emptying the generator returned by
`CassetteContextDecorator._patch_generator` when an exception happens to
skip the `cassette._save` call, however this had the side effect of not
emptying the `ExitStack` created inside the generator which contains the
`_patch.__exit__` calls to remove the patches.

This was innocuous in CPython, which uses a reference counting garbage
collector so the `ExitStack` was immediately collected after losing
scope and therefore its `__exit__` method executed.
Pypy, on the other hand, uses a generational garbage collector so its
objects may survive more time, enough for the `ExitStack` not called
until much later, which may cause the patches to live more than expected
when `record_on_exception=False`.

This was found because the test
`test_nesting_context_managers_by_checking_references_of_http_connection`
was failing because it was executed after
`test_dont_record_on_exception`.

Now the cassette instance is saved inside the `CassetteContextDecorator`
instance to have better control on where to save the cassette, and moved
the `cassette._save` call from the `_patch_generator` method to the
`__exit__` method to be free to empty the generator and remove the
patches always.
2022-10-31 22:43:03 -03:00
Terseus
2980bfccde Fix lint errors 2022-10-31 22:43:03 -03:00
Dan Passaro
7599f4d50a Fix Py3 tests using b'' literal 2022-10-31 22:43:03 -03:00
Dan Passaro
995020bf06 Add record_on_exception flag.
Defaults to True, which maintains historical behavior.

Fixes #205.
2022-10-31 22:43:03 -03:00
Jair Henrique
423ccaa40b Set fail-fast to false on CI 2022-10-31 22:18:30 -03:00
Josef
526fdbb194 Add Path handling to use_cassette and to filesystem.py persister
* now it is possible to use path from pathlib
2022-10-31 22:15:14 -03:00
Evgeni Golov
511d0ab855 add python 3.11 support 2022-10-31 09:00:35 -03:00
Jair Henrique
60ac99c907 Run lint on CI 2022-10-14 10:53:54 -03:00
dependabot[bot]
57dee93e11 build(deps): bump actions/checkout from 3.0.2 to 3.1.0
Bumps [actions/checkout](https://github.com/actions/checkout) from 3.0.2 to 3.1.0.
- [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.0.2...v3.1.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-10-11 10:34:41 -03:00
Jair Henrique
0eece7f96e Add isort to code lint 2022-10-09 11:35:37 -03:00
Chris Wesseling
eb59d871b4 Handles empty responses with gzip/deflate encoding.
Closes #661
2022-10-09 11:04:44 -03:00
Kevin McCarthy
f3f66086a1 Release v4.2.1 2022-08-31 14:14:32 -05:00
Ville Skyttä
c3767c2fdb fix(aiohttp): record body of first request in redirect chain
The first could be e.g. a POST with one, only the remaining ones are
expected to be GETs without one.
2022-08-31 14:11:55 -05:00
Joel Shprentz
6fff3ab952 Add match_on option scheme in configuration.rst
Minor documentation corrections: The list of ``match_on`` options was missing ``scheme``. Also, an unneeded period appeared after URI.
2022-08-04 08:42:36 -03:00
Tim Gates
35378d046f docs: Fix a few typos
There are small typos in:
- tests/integration/test_record_mode.py
- tests/integration/test_stubs.py
- vcr/stubs/__init__.py

Fixes:
- Should read `repercussions` rather than `repurcussions`.
- Should read `superclass` rather than `superclas`.
- Should read `cassette` rather than `casssette`.

Signed-off-by: Tim Gates <tim.gates@iress.com>
2022-08-04 08:42:13 -03:00
Kevin McCarthy
9c46831a8e release 4.2.0 2022-06-29 16:53:45 -05:00
dependabot[bot]
fe596447ec build(deps): bump actions/setup-python from 3 to 4
Bumps [actions/setup-python](https://github.com/actions/setup-python) from 3 to 4.
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](https://github.com/actions/setup-python/compare/v3...v4)

---
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>
2022-06-23 08:29:52 -03:00
Andre Ambrosio Boechat
be1035fd5d Check if query params in the string URL are also included in the final params 2022-06-13 09:54:20 -03:00
Andre Ambrosio Boechat
eb96c590ff Copy the way aiohttp builds the request url with query parameters 2022-06-13 09:54:20 -03:00
Andre Ambrosio Boechat
7add8c0bab Don't assume params to be a dictionary
aiohttp also fails with pass parameter values with types other than
string, integer or float.
2022-06-13 09:54:20 -03:00
Justintime50
b1bc5c3a02 fix: docs wording from request to response 2022-05-22 11:26:06 -03:00
dependabot[bot]
86806aa9bc Bump actions/checkout from 3.0.1 to 3.0.2
Bumps [actions/checkout](https://github.com/actions/checkout) from 3.0.1 to 3.0.2.
- [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.0.1...v3.0.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-04-26 12:52:37 -03:00
Paulo Romeira
7e73085331 aiohttp: Add tests to aiohttp allow_redirects option 2022-04-19 11:10:41 -03:00
Paulo Romeira
3da66c8dee aiohttp: Add support to allow_redirects option 2022-04-19 11:10:41 -03:00
immerrr
f5ea0304da Use pytest-httpbin version with fix for HTTPS redirects 2022-04-19 09:32:25 -03:00
immerrr
25f715bc42 Fix httpx support (select between allow_redirects/follow_redirects) 2022-04-19 09:32:25 -03:00
dependabot[bot]
7d7164d7c7 Bump actions/setup-python from 2 to 3.1.0
Bumps [actions/setup-python](https://github.com/actions/setup-python) from 2 to 3.1.0.
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](https://github.com/actions/setup-python/compare/v2...v3.1.0)

---
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>
2022-04-19 09:11:17 -03:00
dependabot[bot]
fb065751dc Bump actions/checkout from 3.0.0 to 3.0.1
Bumps [actions/checkout](https://github.com/actions/checkout) from 3.0.0 to 3.0.1.
- [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.0.0...v3.0.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-04-19 09:06:03 -03:00
Jair Henrique
874cf06407 Drop support to python 3.6 2022-04-18 17:54:46 -03:00
dependabot[bot]
b0e83986f0 Bump actions/checkout from 2.4.0 to 3.0.0
Bumps [actions/checkout](https://github.com/actions/checkout) from 2.4.0 to 3.0.0.
- [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/v2.4.0...v3.0.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-04-16 17:12:21 -03:00
Ivan Malison
8c0bb73658 Merge pull request #615 from cclauss/patch-1
pip install -upgrade pip
2021-11-03 08:27:17 -06:00
Christian Clauss
43182d97de pip install --upgrade pip 2021-11-03 14:32:39 +01:00
Christian Clauss
193210de49 pip install -upgrade pip 2021-11-03 14:30:37 +01:00
Christian Clauss
e05ebca5e5 Fix typos discovered by codespell 2021-11-03 08:10:17 -03:00
Jair Henrique
cd72278062 Fix urllib redirect tests 2021-11-03 08:09:51 -03:00
Jair Henrique
3c7b791783 Fix httplib2 tests 2021-11-03 08:09:36 -03:00
dependabot[bot]
7592efb8d9 Bump actions/checkout from 1 to 2.4.0
Bumps [actions/checkout](https://github.com/actions/checkout) from 1 to 2.4.0.
- [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/v1...v2.4.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2021-11-03 08:09:20 -03:00
Jair Henrique
5b2fc2712e Change ci badge to github actions 2021-11-02 14:57:19 -10:00
Jair Henrique
c596a160b3 Move from travis to github actions 2021-11-02 10:31:00 -10:00
Ivan Malison
e68aa84649 Merge pull request #602 from scop/spelling
Spelling fixes
2021-08-16 04:04:29 +00:00
Ville Skyttä
678d56f608 Spelling fixes 2021-08-16 06:56:36 +03:00
Ivan Malison
d4927627c3 Merge pull request #582 from scop/fix/filter-dict-post-data-parameters
fix(filters): make work with dict body parameters, such as aiohttp
2021-08-15 20:22:31 +00:00
Ivan Malison
61b83aca7f Merge pull request #554 from AthulMuralidhar/patch-1
Change urllib2 to ulrlib in accordance with Py 3.5
2021-08-15 20:17:23 +00:00
Ivan Malison
0ac66f4413 Merge pull request #564 from jairhenrique/drop-35
Drop support to deprecated Python (<3.6)
2021-08-15 20:16:21 +00:00
Ville Skyttä
000f7448a7 fix(filters): make work with dict body parameters, such as aiohttp
Closes https://github.com/kevin1024/vcrpy/issues/398
2021-04-15 22:40:41 +03:00
Kevin McCarthy
c79a06f639 release version 4.1.1 2020-10-09 15:38:37 -05:00
Jair Henrique
08ef4a8bc4 Drop support to deprecated Python (<3.6) 2020-10-06 10:17:07 -03:00
Jair Henrique
4991d3d22b Fix tornado tests 2020-10-05 09:45:55 -05:00
Jair Henrique
b2193828ab Fix matrix of tests 2020-10-05 09:45:55 -05:00
Jair Henrique
9d414c8841 Fix lint erros 2020-10-05 09:45:55 -05:00
Jair Henrique
a172207df0 Fix integration with httpx>0.15.0 2020-10-05 09:45:55 -05:00
Athul Muralidhar
dda16ef1e5 Change urllib2 to ulrlib
the change is required because the urllib2 is now moved to urllib in python 3.5
2020-08-19 11:31:59 +02:00
Aaron Robson
1d0fdbaa13 fix #429: include a trailing newline in the JSON cassette format
It is a common convention for text files (esp. in Linux) to end with a newline.
2020-08-05 17:27:30 -05:00
Kevin McCarthy
a351621d92 update changelog and bump version 2020-07-31 09:40:27 -05:00
Hernan Ezequiel Di Giorgi
f387950486 support allow_redirects option 2020-07-24 15:34:57 -05:00
Hernan Ezequiel Di Giorgi
8529c46f00 update tests
- Use mockbin for redirections.
- Mock synchronous requests too.
2020-07-24 15:34:57 -05:00
Gintaras Jak
5afa8f703a Ignore utf8 decoding errors in content 2020-07-24 15:34:57 -05:00
Hernan Ezequiel Di Giorgi
641d9e5d49 list httpx in the compatibility list 2020-07-24 15:34:57 -05:00
Martynas Mickevičius
0ef400195b Fix linter 2020-07-24 15:34:57 -05:00
Martynas Mickevičius
133423ce94 Reuse value from Location header on redirect
Because the recoded requests are using original value.
Changing the value on redirect prevents finding the
matching response.
2020-07-24 15:34:57 -05:00
Martynas Mickevičius
7d2d29de12 Extract cookies to client on every response
This is required because previous extraction code is now patched out by vcpy.

Also handle headers with same key in responses.
2020-07-24 15:34:57 -05:00
Martynas Mickevičius
023e41bb4c Run tests on Travis CI 2020-07-24 15:34:57 -05:00
Hernan Ezequiel Di Giorgi
6ae93ac820 fix linting errors in test_httpx.py 2020-07-24 15:34:57 -05:00
Hernan Ezequiel Di Giorgi
04b7f4fc65 test that works behind proxy 2020-07-24 15:34:57 -05:00
Hernan Ezequiel Di Giorgi
936feb7748 fix redirection url 2020-07-24 15:34:57 -05:00
Hernan Ezequiel Di Giorgi
79d26ebb43 make it work with gzipped data 2020-07-24 15:34:57 -05:00
Hernan Ezequiel Di Giorgi
b8024de1b8 add missing pytest-asyncio dep for httpx 2020-07-24 15:34:57 -05:00
Hernan Ezequiel Di Giorgi
2f94d06e9b add httpx support 2020-07-24 15:34:57 -05:00
Tyson Holub
042ee790e2 add allow_playback_repeats option to Cassette 2020-07-15 16:09:17 -05:00
Paulo Romeira
a249781b97 aiohttp: Use record_mode enum in tests 2020-07-15 13:52:00 -05:00
Paulo Romeira
b64e93aff2 aiohttp: Check if the response is playable before redirecting 2020-07-15 13:52:00 -05:00
Paulo Romeira
4897a8e692 aiohttp: Fix linting 2020-07-15 13:52:00 -05:00
Paulo Romeira
15d79e5b78 aiphttp: Add test for cookies on redirect 2020-07-15 13:52:00 -05:00
Paulo Romeira
1b9f80d741 aiohttp: Ignore cookie expiration date 2020-07-15 13:52:00 -05:00
Paulo Romeira
20fb283e97 aiohttp: Update cookies with redirection responses 2020-07-15 13:52:00 -05:00
Paulo Romeira
e868b64922 aiohttp: Fix next_url redirect for absolute paths 2020-07-15 13:52:00 -05:00
Paulo Romeira
69cecdbda7 aiohttp: Fix tests 2020-07-15 13:52:00 -05:00
Paulo Romeira
be53091ae5 aiohttp: Replace aiohttp package for module 2020-07-15 13:52:00 -05:00
Paulo Romeira
ba91053485 aiohttp: Raise CannotOverwriteExistingCassette if not found 2020-07-15 13:52:00 -05:00
Paulo Romeira
837992767e aiohttp: Fix tests 2020-07-15 13:52:00 -05:00
Paulo Romeira
cd0907ffaf aiohttp: Serialize request headers 2020-07-15 13:52:00 -05:00
Paulo Romeira
77d838e0fc aiohttp: Fix code linting 2020-07-15 13:52:00 -05:00
Paulo Romeira
5362db2ebb aiohttp: Update tests 2020-07-15 13:52:00 -05:00
Paulo Romeira
f9ce14d29a aiohttp: Fix vcr_request cookies 2020-07-15 13:52:00 -05:00
Paulo Romeira
5242e68cd1 aiohttp: Fix code linting 2020-07-15 13:52:00 -05:00
Paulo Romeira
9817a8bda5 aiohttp: Add request cookies to "Cookie" header 2020-07-15 13:52:00 -05:00
Paulo Romeira
6e1768b85b aiohttp: Update ClientSession's cookie_jar with response cookies 2020-07-15 13:52:00 -05:00
Paulo Romeira
062126e50c tests/aiohttp: Add cookies test 2020-07-15 13:52:00 -05:00
Paulo Romeira
438550959f aiohttp: Add response cookies loading 2020-07-15 13:52:00 -05:00
Paulo Romeira
69e4316545 aiohttp: Add support for multiple headers with same key 2020-07-15 13:52:00 -05:00
Jair Henrique
2f53776ffb Use mockbin to fix redirect test 2020-07-15 12:18:23 -05:00
Ivan Malison
535efe1eb9 Merge pull request #514 from timgates42/bugfix/typo_cassette
Fix simple typo: cassete -> cassette
2020-07-01 20:19:41 -07:00
Ivan Malison
eb2e226bb8 Merge pull request #530 from aaronbannin/record-mode-enum
Enumerate Record Mode Values
2020-07-01 20:08:06 -07:00
Ivan Malison
8fe2ab6d06 Merge pull request #511 from royjs/master
Do not redirect on 304 for aiohttp
2020-06-29 14:25:45 -07:00
Aaron Bannin
6ac535f18d fix lints 2020-05-24 21:12:41 -07:00
Aaron Bannin
bceaab8b88 add carraige returns 2020-05-24 20:44:21 -07:00
Aaron Bannin
0c2bbe0d51 enumerate record_mode values 2020-05-24 20:15:01 -07:00
Kevin McCarthy
2e5fdd36d5 Update README.rst 2020-04-19 08:46:10 -05:00
Tim Gates
f8b9a41f13 Fix simple typo: cassete -> cassette
Closes #513
2020-02-26 18:04:31 +11:00
Jean-Sebastien Roy
6e040030b8 Do not redirect on 304 for aiohttp 2020-01-27 11:24:02 -05:00
Josh Peak
accffa8ea2 Correct mock imports. Fixes #504 (#505)
* Correct mock imports. Fixes #504

Update changelog

Revert accidental import deletion

* Remove move dependency that was masking error from tox.ini
2019-12-20 22:08:25 +11:00
74 changed files with 1468 additions and 509 deletions

11
.github/dependabot.yml vendored Normal file
View File

@@ -0,0 +1,11 @@
version: 2
updates:
- package-ecosystem: pip
directory: "/"
schedule:
interval: weekly
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: weekly

40
.github/workflows/main.yml vendored Normal file
View File

@@ -0,0 +1,40 @@
name: Test
on:
push:
branches:
- master
pull_request:
workflow_dispatch:
jobs:
build:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", "pypy-3.8"]
steps:
- name: Install libgnutls28-dev
run: |
sudo apt update -q
sudo apt install -q -y libgnutls28-dev libcurl4-gnutls-dev
- uses: actions/checkout@v3.5.2
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
- name: Install project dependencies
run: |
pip install --upgrade pip
pip install codecov tox tox-gh-actions
- name: Run tests with tox
run: tox
- name: Run coverage
run: codecov

View File

@@ -1,32 +0,0 @@
dist: xenial
language: python
before_install: openssl version
env:
# global:
# - secure: AifoKzwhjV94cmcQZrdQmqRu/9rkZZvWpwBv1daeAQpLOKFPGsOm3D+x2cSw9+iCfkgDZDfqQVv1kCaFVxTll8v8jTq5SJdqEY0NmGWbj/UkNtShh609oRDsuzLxAEwtVKYjf/h8K2BRea+bl1tGkwZ2vtmYS6dxNlAijjWOfds=
# - secure: LBSEg/gMj4u4Hrpo3zs6Y/1mTpd2RtcN49mZIFgTdbJ9IhpiNPqcEt647Lz94F9Eses2x2WbNuKqZKZZReY7QLbEzU1m0nN5jlaKrjcG5NR5clNABfFFyhgc0jBikyS4abAG8jc2efeaTrFuQwdoF4sE8YiVrkiVj2X5Xoi6sBk=
matrix:
- TOX_SUFFIX="requests"
- TOX_SUFFIX="httplib2"
- TOX_SUFFIX="boto3"
- TOX_SUFFIX="urllib3"
- TOX_SUFFIX="tornado4"
- TOX_SUFFIX="aiohttp"
matrix:
include:
# Only run lint on a single 3.x
- env: TOX_SUFFIX="lint"
python: "3.7"
python:
- "3.5"
- "3.6"
- "3.7"
- "3.8"
- "pypy3"
install:
- pip install tox-travis codecov
- if [[ $TOX_SUFFIX != 'lint' ]]; then python setup.py install ; fi
script:
- tox -e "${TOX_SUFFIX}"
after_success:
- codecov

View File

@@ -47,6 +47,11 @@ VCR.py will detect the absence of a cassette file and once again record
all HTTP interactions, which will update them to correspond to the new all HTTP interactions, which will update them to correspond to the new
API. API.
Usage with Pytest
-----------------
There is a library to provide some pytest fixtures called pytest-recording https://github.com/kiwicom/pytest-recording
License License
------- -------
@@ -57,8 +62,8 @@ more details
:target: https://pypi.python.org/pypi/vcrpy :target: https://pypi.python.org/pypi/vcrpy
.. |Python versions| image:: https://img.shields.io/pypi/pyversions/vcrpy.svg .. |Python versions| image:: https://img.shields.io/pypi/pyversions/vcrpy.svg
:target: https://pypi.python.org/pypi/vcrpy :target: https://pypi.python.org/pypi/vcrpy
.. |Build Status| image:: https://secure.travis-ci.org/kevin1024/vcrpy.svg?branch=master .. |Build Status| image:: https://github.com/kevin1024/vcrpy/actions/workflows/main.yml/badge.svg
:target: http://travis-ci.org/kevin1024/vcrpy :target: https://github.com/kevin1024/vcrpy/actions
.. |Gitter| image:: https://badges.gitter.im/Join%20Chat.svg .. |Gitter| image:: https://badges.gitter.im/Join%20Chat.svg
:alt: Join the chat at https://gitter.im/kevin1024/vcrpy :alt: Join the chat at https://gitter.im/kevin1024/vcrpy
:target: https://gitter.im/kevin1024/vcrpy?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge :target: https://gitter.im/kevin1024/vcrpy?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge

View File

@@ -33,6 +33,8 @@ consider part of the API. The fields are as follows:
been played back. been played back.
- ``responses_of(request)``: Access the responses that match a given - ``responses_of(request)``: Access the responses that match a given
request request
- ``allow_playback_repeats``: A boolean indicating whether responses
can be played back more than once.
The ``Request`` object has the following properties: The ``Request`` object has the following properties:
@@ -269,7 +271,7 @@ You can also do response filtering with the
similar to the above ``before_record_request`` - you can similar to the above ``before_record_request`` - you can
mutate the response, or return ``None`` to avoid recording mutate the response, or return ``None`` to avoid recording
the request and response altogether. For example to hide the request and response altogether. For example to hide
sensitive data from the request body: sensitive data from the response body:
.. code:: python .. code:: python
@@ -386,3 +388,41 @@ VCR.py allows to rewind a cassette in order to replay it inside the same functio
assert cass.all_played assert cass.all_played
cass.rewind() cass.rewind()
assert not cass.all_played assert not cass.all_played
Playback Repeats
----------------
By default, each response in a cassette can only be matched and played back
once while the cassette is in use, unless the cassette is rewound.
If you want to allow playback repeats without rewinding the cassette, use
the Cassette ``allow_playback_repeats`` option.
.. code:: python
with vcr.use_cassette('fixtures/vcr_cassettes/synopsis.yaml', allow_playback_repeats=True) as cass:
for x in range(10):
response = urllib2.urlopen('http://www.zombo.com/').read()
assert cass.all_played
Discards Cassette on Errors
---------------------------
By default VCR will save the cassette file even when there is any error inside
the enclosing context/test.
If you want to save the cassette only when the test succeeds, set the Cassette
``record_on_exception`` option to ``False``.
.. code:: python
try:
my_vcr = VCR(record_on_exception=False)
with my_vcr.use_cassette('fixtures/vcr_cassettes/synopsis.yaml') as cass:
response = urllib2.urlopen('http://www.zombo.com/').read()
raise RuntimeError("Oops, something happened")
except RuntimeError:
pass
# Since there was an exception, the cassette file hasn't been created.
assert not os.path.exists('fixtures/vcr_cassettes/synopsis.yaml')

View File

@@ -1,16 +1,45 @@
Changelog Changelog
--------- ---------
For a full list of triaged issues, bugs and PRs and what release they are targetted for please see the following link. For a full list of triaged issues, bugs and PRs and what release they are targeted for please see the following link.
`ROADMAP MILESTONES <https://github.com/kevin1024/vcrpy/milestones>`_ `ROADMAP MILESTONES <https://github.com/kevin1024/vcrpy/milestones>`_
All help in providing PRs to close out bug issues is appreciated. Even if that is providing a repo that fully replicates issues. We have very generous contributors that have added these to bug issues which meant another contributor picked up the bug and closed it out. All help in providing PRs to close out bug issues is appreciated. Even if that is providing a repo that fully replicates issues. We have very generous contributors that have added these to bug issues which meant another contributor picked up the bug and closed it out.
- UNRELEASED - 4.3.0
- ... - Add support for Python 3.11 (Thanks @evgeni)
- Drop support for botocore <1.11.0 and requests <2.16.2 (thanks @hartwork)
- Bugfix: decode_compressed_response raises exception on empty responses. Thanks @CharString
- Don't save requests from decorated tests if decorated test fails (thanks @dan-passaro)
- Fix not calling all the exit stack when record_on_exception is False (thanks @Terseus)
- Various CI, documentation, testing, and formatting improvements (Thanks @jairhenrique, @dan-passaro, @hartwork, and Terseus)
- 4.2.1
- Fix a bug where the first request in a redirect chain was not being recorded with aiohttp
- Various typos and small fixes, thanks @jairhenrique, @timgates42
- 4.2.0
- Drop support for python < 3.7, thanks @jairhenrique, @IvanMalison, @AthulMuralidhar
- Various aiohtt bigfixes (thanks @pauloromeira and boechat107)
- Bugfix: filter_post_data_parameters not working with aiohttp. Thank you @vprakashplanview, @scop, @jairhenrique, and @cinemascop89
- Bugfix: Some random misspellings (thanks @scop)
- Migrate the CI suite to Github Actions from Travis (thanks @jairhenrique and @cclauss)
- Various documentation and code misspelling fixes (thanks @scop and @Justintime50)
- Bugfix: httpx support (select between allow_redirects/follow_redirects) (thanks @immerrr)
- Bugfix: httpx support (select between allow_redirects/follow_redirects) (thanks @immerrr)
- 4.1.1
- Fix HTTPX support for versions greater than 0.15 (thanks @jairhenrique)
- Include a trailing newline on json cassettes (thanks @AaronRobson)
- 4.1.0
- Add support for httpx!! (thanks @herdigiorgi)
- Add the new `allow_playback_repeats` option (thanks @tysonholub)
- Several aiohttp improvements (cookie support, multiple headers with same key) (Thanks @pauloromeira)
- Use enums for record modes (thanks @aaronbannin)
- Bugfix: Do not redirect on 304 in aiohttp (Thanks @royjs)
- Bugfix: Fix test suite by switching to mockbin (thanks @jairhenrique)
- 4.0.2
- Fix mock imports as reported in #504 by @llybin. Thank you.
- 4.0.1
- Fix logo alignment for PyPI
- 4.0.0 - 4.0.0
- Remove Python2 support (@hugovk) - Remove Python2 support (@hugovk)
- Add Python 3.8 TravisCI support (@neozenith) - Add Python 3.8 TravisCI support (@neozenith)
@@ -90,7 +119,7 @@ All help in providing PRs to close out bug issues is appreciated. Even if that i
- decode_compressed_response option and filter (thanks @jayvdb). - decode_compressed_response option and filter (thanks @jayvdb).
- 1.7.4 [#217] - 1.7.4 [#217]
- Make use_cassette decorated functions actually return a value (thanks @bcen). - Make use_cassette decorated functions actually return a value (thanks @bcen).
- [#199] Fix path transfromation defaults. - [#199] Fix path transformation defaults.
- Better headers dictionary management. - Better headers dictionary management.
- 1.7.3 [#188] - 1.7.3 [#188]
- ``additional_matchers`` kwarg on ``use_cassette``. - ``additional_matchers`` kwarg on ``use_cassette``.
@@ -193,7 +222,7 @@ All help in providing PRs to close out bug issues is appreciated. Even if that i
- 0.3.4 - 0.3.4
- Bugfix: close file before renaming it. This fixes an issue on Windows. Thanks @smallcode for the fix. - Bugfix: close file before renaming it. This fixes an issue on Windows. Thanks @smallcode for the fix.
- 0.3.3 - 0.3.3
- Bugfix for error message when an unreigstered custom matcher was used - Bugfix for error message when an unregistered custom matcher was used
- 0.3.2 - 0.3.2
- Fix issue with new config syntax and the ``match_on`` parameter. Thanks, @chromy! - Fix issue with new config syntax and the ``match_on`` parameter. Thanks, @chromy!
- 0.3.1 - 0.3.1

View File

@@ -94,7 +94,7 @@ version = release = find_version("..", "vcr", "__init__.py")
# #
# This is also used if you do content translation via gettext catalogs. # This is also used if you do content translation via gettext catalogs.
# Usually you set "language" from the command line for these cases. # Usually you set "language" from the command line for these cases.
language = None language = "en"
# There are two options for replacing |today|: either, you set today to some # There are two options for replacing |today|: either, you set today to some
# non-false value, then it is used: # non-false value, then it is used:

View File

@@ -42,7 +42,8 @@ This can be configured by changing the ``match_on`` setting.
The following options are available : The following options are available :
- method (for example, POST or GET) - method (for example, POST or GET)
- uri (the full URI.) - uri (the full URI)
- scheme (for example, HTTP or HTTPS)
- host (the hostname of the server receiving the request) - host (the hostname of the server receiving the request)
- port (the port of the server receiving the request) - port (the port of the server receiving the request)
- path (the path of the request) - path (the path of the request)

View File

@@ -96,11 +96,11 @@ The test suite is pretty big and slow, but you can tell tox to only run specific
tox -e {pyNN}-{HTTP_LIBRARY} -- <pytest flags passed through> tox -e {pyNN}-{HTTP_LIBRARY} -- <pytest flags passed through>
tox -e py36-requests -- -v -k "'test_status_code or test_gzip'" tox -e py37-requests -- -v -k "'test_status_code or test_gzip'"
tox -e py37-requests -- -v --last-failed tox -e py37-requests -- -v --last-failed
This will run only tests that look like ``test_status_code`` or This will run only tests that look like ``test_status_code`` or
``test_gzip`` in the test suite, and only in the python 3.6 environment ``test_gzip`` in the test suite, and only in the python 3.7 environment
that has ``requests`` installed. that has ``requests`` installed.
Also, in order for the boto tests to run, you will need an AWS key. Also, in order for the boto tests to run, you will need an AWS key.
@@ -130,10 +130,10 @@ in this example::
pip install tox tox-pyenv pip install tox tox-pyenv
# Install supported versions (at time of writing), this does not activate them # Install supported versions (at time of writing), this does not activate them
pyenv install 3.5.9 3.6.9 3.7.5 3.8.0 pypy3.6-7.2.0 pyenv install 3.7.5 3.8.0 pypy3.8
# This activates them # This activates them
pyenv local 3.5.9 3.6.9 3.7.5 3.8.0 pypy3.6-7.2.0 pyenv local 3.7.5 3.8.0 pypy3.8
# Run the whole test suite # Run the whole test suite
tox tox

View File

@@ -9,7 +9,7 @@ with pip::
Compatibility Compatibility
------------- -------------
VCR.py supports Python 3.5+, and `pypy <http://pypy.org>`__. VCR.py supports Python 3.7+, and `pypy <http://pypy.org>`__.
The following HTTP libraries are supported: The following HTTP libraries are supported:
@@ -22,6 +22,7 @@ The following HTTP libraries are supported:
- ``tornado.httpclient`` - ``tornado.httpclient``
- ``urllib2`` - ``urllib2``
- ``urllib3`` - ``urllib3``
- ``httpx``
Speed Speed
----- -----

View File

@@ -4,10 +4,10 @@ Usage
.. code:: python .. code:: python
import vcr import vcr
import urllib2 import urllib
with vcr.use_cassette('fixtures/vcr_cassettes/synopsis.yaml'): with vcr.use_cassette('fixtures/vcr_cassettes/synopsis.yaml'):
response = urllib2.urlopen('http://www.iana.org/domains/reserved').read() response = urllib.request.urlopen('http://www.iana.org/domains/reserved').read()
assert 'Example domains' in response assert 'Example domains' in response
Run this test once, and VCR.py will record the HTTP request to Run this test once, and VCR.py will record the HTTP request to
@@ -25,7 +25,7 @@ look like this:
@vcr.use_cassette('fixtures/vcr_cassettes/synopsis.yaml') @vcr.use_cassette('fixtures/vcr_cassettes/synopsis.yaml')
def test_iana(): def test_iana():
response = urllib2.urlopen('http://www.iana.org/domains/reserved').read() response = urllib.request.urlopen('http://www.iana.org/domains/reserved').read()
assert 'Example domains' in response assert 'Example domains' in response
When using the decorator version of ``use_cassette``, it is possible to When using the decorator version of ``use_cassette``, it is possible to
@@ -35,7 +35,7 @@ omit the path to the cassette file.
@vcr.use_cassette() @vcr.use_cassette()
def test_iana(): def test_iana():
response = urllib2.urlopen('http://www.iana.org/domains/reserved').read() response = urllib.request.urlopen('http://www.iana.org/domains/reserved').read()
assert 'Example domains' in response assert 'Example domains' in response
In this case, the cassette file will be given the same name as the test In this case, the cassette file will be given the same name as the test

View File

@@ -1,2 +1,9 @@
[tool.black] [tool.black]
line-length=110 line-length=110
[tool.isort]
line_length = 110
known_first_party = "vcrpy"
multi_line_output = 3
use_parentheses = true
include_trailing_comma = true

View File

@@ -4,4 +4,4 @@
# If you are getting an INVOCATION ERROR for this script then there is # If you are getting an INVOCATION ERROR for this script then there is
# a good chance you are running on Windows. # a good chance you are running on Windows.
# You can and should use WSL for running tox on Windows when it calls bash scripts. # You can and should use WSL for running tox on Windows when it calls bash scripts.
REQUESTS_CA_BUNDLE=`python -m pytest_httpbin.certs` py.test $* REQUESTS_CA_BUNDLE=`python -m pytest_httpbin.certs` pytest $*

View File

@@ -5,7 +5,7 @@ import os
import re import re
import sys import sys
from setuptools import setup, find_packages from setuptools import find_packages, setup
from setuptools.command.test import test as TestCommand from setuptools.command.test import test as TestCommand
long_description = open("README.rst", "r").read() long_description = open("README.rst", "r").read()
@@ -46,8 +46,7 @@ install_requires = [
"PyYAML", "PyYAML",
"wrapt", "wrapt",
"six>=1.5", "six>=1.5",
'yarl; python_version>="3.6"', "yarl",
'yarl<1.4; python_version=="3.5"',
] ]
setup( setup(
@@ -60,7 +59,7 @@ setup(
author_email="me@kevinmccarthy.org", author_email="me@kevinmccarthy.org",
url="https://github.com/kevin1024/vcrpy", url="https://github.com/kevin1024/vcrpy",
packages=find_packages(exclude=["tests*"]), packages=find_packages(exclude=["tests*"]),
python_requires=">=3.5", python_requires=">=3.7",
install_requires=install_requires, install_requires=install_requires,
license="MIT", license="MIT",
tests_require=["pytest", "mock", "pytest-httpbin"], tests_require=["pytest", "mock", "pytest-httpbin"],
@@ -70,10 +69,11 @@ setup(
"Intended Audience :: Developers", "Intended Audience :: Developers",
"Programming Language :: Python", "Programming Language :: Python",
"Programming Language :: Python :: 3", "Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.5",
"Programming Language :: Python :: 3.6",
"Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3 :: Only",
"Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: CPython",
"Programming Language :: Python :: Implementation :: PyPy", "Programming Language :: Python :: Implementation :: PyPy",

View File

@@ -2,7 +2,6 @@
import asyncio import asyncio
import aiohttp import aiohttp
from aiohttp.test_utils import TestClient
async def aiohttp_request(loop, method, url, output="text", encoding="utf-8", content_type=None, **kwargs): async def aiohttp_request(loop, method, url, output="text", encoding="utf-8", content_type=None, **kwargs):

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.12.1
method: GET
uri: https://mockbin.org/headers
response:
content: "{\n \"headers\": {\n \"Accept\": \"*/*\", \n \"Accept-Encoding\"\
: \"gzip, deflate, br\", \n \"Host\": \"httpbin.org\", \n \"User-Agent\"\
: \"python-httpx/0.12.1\", \n \"X-Amzn-Trace-Id\": \"Root=1-5ea778c9-ea76170da792abdbf7614067\"\
\n }\n}\n"
headers:
access-control-allow-credentials:
- 'true'
access-control-allow-origin:
- '*'
connection:
- keep-alive
content-length:
- '226'
content-type:
- application/json
date:
- Tue, 28 Apr 2020 00:28:57 GMT
server:
- gunicorn/19.9.0
via:
- my_own_proxy
http_version: HTTP/1.1
status_code: 200
version: 1

View File

@@ -0,0 +1,17 @@
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"

View File

@@ -1,5 +1,6 @@
import contextlib import contextlib
import logging import logging
import urllib.parse
import pytest import pytest
@@ -7,6 +8,7 @@ asyncio = pytest.importorskip("asyncio")
aiohttp = pytest.importorskip("aiohttp") aiohttp = pytest.importorskip("aiohttp")
import vcr # noqa: E402 import vcr # noqa: E402
from .aiohttp_utils import aiohttp_app, aiohttp_request # noqa: E402 from .aiohttp_utils import aiohttp_app, aiohttp_request # noqa: E402
@@ -32,14 +34,9 @@ def post(url, output="text", **kwargs):
return request("POST", url, output="text", **kwargs) return request("POST", url, output="text", **kwargs)
@pytest.fixture(params=["https", "http"]) def test_status(tmpdir, mockbin_request_url):
def scheme(request): url = mockbin_request_url
"""Fixture that returns both http and https."""
return request.param
def test_status(tmpdir, scheme):
url = scheme + "://httpbin.org"
with vcr.use_cassette(str(tmpdir.join("status.yaml"))): with vcr.use_cassette(str(tmpdir.join("status.yaml"))):
response, _ = get(url) response, _ = get(url)
@@ -50,8 +47,8 @@ def test_status(tmpdir, scheme):
@pytest.mark.parametrize("auth", [None, aiohttp.BasicAuth("vcrpy", "test")]) @pytest.mark.parametrize("auth", [None, aiohttp.BasicAuth("vcrpy", "test")])
def test_headers(tmpdir, scheme, auth): def test_headers(tmpdir, auth, mockbin_request_url):
url = scheme + "://httpbin.org" url = mockbin_request_url
with vcr.use_cassette(str(tmpdir.join("headers.yaml"))): with vcr.use_cassette(str(tmpdir.join("headers.yaml"))):
response, _ = get(url, auth=auth) response, _ = get(url, auth=auth)
@@ -66,8 +63,9 @@ def test_headers(tmpdir, scheme, auth):
assert "yarl.URL" not in cassette.data[0] assert "yarl.URL" not in cassette.data[0]
def test_case_insensitive_headers(tmpdir, scheme): def test_case_insensitive_headers(tmpdir, mockbin_request_url):
url = scheme + "://httpbin.org" url = mockbin_request_url
with vcr.use_cassette(str(tmpdir.join("whatever.yaml"))): with vcr.use_cassette(str(tmpdir.join("whatever.yaml"))):
_, _ = get(url) _, _ = get(url)
@@ -78,8 +76,9 @@ def test_case_insensitive_headers(tmpdir, scheme):
assert cassette.play_count == 1 assert cassette.play_count == 1
def test_text(tmpdir, scheme): def test_text(tmpdir, mockbin_request_url):
url = scheme + "://httpbin.org" url = mockbin_request_url
with vcr.use_cassette(str(tmpdir.join("text.yaml"))): with vcr.use_cassette(str(tmpdir.join("text.yaml"))):
_, response_text = get(url) _, response_text = get(url)
@@ -89,8 +88,8 @@ def test_text(tmpdir, scheme):
assert cassette.play_count == 1 assert cassette.play_count == 1
def test_json(tmpdir, scheme): def test_json(tmpdir, mockbin_request_url):
url = scheme + "://httpbin.org/get" url = mockbin_request_url
headers = {"Content-Type": "application/json"} headers = {"Content-Type": "application/json"}
with vcr.use_cassette(str(tmpdir.join("json.yaml"))): with vcr.use_cassette(str(tmpdir.join("json.yaml"))):
@@ -102,8 +101,8 @@ def test_json(tmpdir, scheme):
assert cassette.play_count == 1 assert cassette.play_count == 1
def test_binary(tmpdir, scheme): def test_binary(tmpdir, mockbin_request_url):
url = scheme + "://httpbin.org/image/png" url = mockbin_request_url + "/image/png"
with vcr.use_cassette(str(tmpdir.join("binary.yaml"))): with vcr.use_cassette(str(tmpdir.join("binary.yaml"))):
_, response_binary = get(url, output="raw") _, response_binary = get(url, output="raw")
@@ -113,23 +112,23 @@ def test_binary(tmpdir, scheme):
assert cassette.play_count == 1 assert cassette.play_count == 1
def test_stream(tmpdir, scheme): def test_stream(tmpdir, mockbin_request_url):
url = scheme + "://httpbin.org/get" url = mockbin_request_url
with vcr.use_cassette(str(tmpdir.join("stream.yaml"))): with vcr.use_cassette(str(tmpdir.join("stream.yaml"))):
resp, body = get(url, output="raw") # Do not use stream here, as the stream is exhausted by vcr _, body = get(url, output="raw") # Do not use stream here, as the stream is exhausted by vcr
with vcr.use_cassette(str(tmpdir.join("stream.yaml"))) as cassette: with vcr.use_cassette(str(tmpdir.join("stream.yaml"))) as cassette:
cassette_resp, cassette_body = get(url, output="stream") _, cassette_body = get(url, output="stream")
assert cassette_body == body assert cassette_body == body
assert cassette.play_count == 1 assert cassette.play_count == 1
@pytest.mark.parametrize("body", ["data", "json"]) @pytest.mark.parametrize("body", ["data", "json"])
def test_post(tmpdir, scheme, body, caplog): def test_post(tmpdir, body, caplog, mockbin_request_url):
caplog.set_level(logging.INFO) caplog.set_level(logging.INFO)
data = {"key1": "value1", "key2": "value2"} data = {"key1": "value1", "key2": "value2"}
url = scheme + "://httpbin.org/post" url = mockbin_request_url
with vcr.use_cassette(str(tmpdir.join("post.yaml"))): with vcr.use_cassette(str(tmpdir.join("post.yaml"))):
_, response_json = post(url, **{body: data}) _, response_json = post(url, **{body: data})
@@ -150,13 +149,14 @@ def test_post(tmpdir, scheme, body, caplog):
), "Log message not found." ), "Log message not found."
def test_params(tmpdir, scheme): def test_params(tmpdir, mockbin_request_url):
url = scheme + "://httpbin.org/get" url = mockbin_request_url + "?d=d"
headers = {"Content-Type": "application/json"} headers = {"Content-Type": "application/json"}
params = {"a": 1, "b": False, "c": "c"} params = {"a": 1, "b": 2, "c": "c"}
with vcr.use_cassette(str(tmpdir.join("get.yaml"))) as cassette: with vcr.use_cassette(str(tmpdir.join("get.yaml"))) as cassette:
_, response_json = get(url, output="json", params=params, headers=headers) _, response_json = get(url, output="json", params=params, headers=headers)
assert response_json["queryString"] == {"a": "1", "b": "2", "c": "c", "d": "d"}
with vcr.use_cassette(str(tmpdir.join("get.yaml"))) as cassette: with vcr.use_cassette(str(tmpdir.join("get.yaml"))) as cassette:
_, cassette_response_json = get(url, output="json", params=params, headers=headers) _, cassette_response_json = get(url, output="json", params=params, headers=headers)
@@ -164,10 +164,10 @@ def test_params(tmpdir, scheme):
assert cassette.play_count == 1 assert cassette.play_count == 1
def test_params_same_url_distinct_params(tmpdir, scheme): def test_params_same_url_distinct_params(tmpdir, mockbin_request_url):
url = scheme + "://httpbin.org/get" url = mockbin_request_url
headers = {"Content-Type": "application/json"} headers = {"Content-Type": "application/json"}
params = {"a": 1, "b": False, "c": "c"} params = {"a": 1, "b": 2, "c": "c"}
with vcr.use_cassette(str(tmpdir.join("get.yaml"))) as cassette: with vcr.use_cassette(str(tmpdir.join("get.yaml"))) as cassette:
_, response_json = get(url, output="json", params=params, headers=headers) _, response_json = get(url, output="json", params=params, headers=headers)
@@ -179,13 +179,12 @@ def test_params_same_url_distinct_params(tmpdir, scheme):
other_params = {"other": "params"} other_params = {"other": "params"}
with vcr.use_cassette(str(tmpdir.join("get.yaml"))) as cassette: with vcr.use_cassette(str(tmpdir.join("get.yaml"))) as cassette:
response, cassette_response_text = get(url, output="text", params=other_params) with pytest.raises(vcr.errors.CannotOverwriteExistingCassetteException):
assert "No match for the request" in cassette_response_text get(url, output="text", params=other_params)
assert response.status == 599
def test_params_on_url(tmpdir, scheme): def test_params_on_url(tmpdir, mockbin_request_url):
url = scheme + "://httpbin.org/get?a=1&b=foo" url = mockbin_request_url + "?a=1&b=foo"
headers = {"Content-Type": "application/json"} headers = {"Content-Type": "application/json"}
with vcr.use_cassette(str(tmpdir.join("get.yaml"))) as cassette: with vcr.use_cassette(str(tmpdir.join("get.yaml"))) as cassette:
@@ -249,8 +248,8 @@ def test_aiohttp_test_client_json(aiohttp_client, tmpdir):
assert cassette.play_count == 1 assert cassette.play_count == 1
def test_redirect(aiohttp_client, tmpdir): def test_redirect(tmpdir, mockbin):
url = "https://httpbin.org/redirect/2" url = mockbin + "/redirect/302/2"
with vcr.use_cassette(str(tmpdir.join("redirect.yaml"))): with vcr.use_cassette(str(tmpdir.join("redirect.yaml"))):
response, _ = get(url) response, _ = get(url)
@@ -273,13 +272,30 @@ def test_redirect(aiohttp_client, tmpdir):
assert cassette_response.request_info.real_url == response.request_info.real_url assert cassette_response.request_info.real_url == response.request_info.real_url
def test_double_requests(tmpdir): def test_not_modified(tmpdir, mockbin):
"""We should capture, record, and replay all requests and response chains, """It doesn't try to redirect on 304"""
even if there are duplicate ones. url = mockbin + "/status/304"
We should replay in the order we saw them. with vcr.use_cassette(str(tmpdir.join("not_modified.yaml"))):
response, _ = get(url)
with vcr.use_cassette(str(tmpdir.join("not_modified.yaml"))) as cassette:
cassette_response, _ = get(url)
assert cassette_response.status == 304
assert response.status == 304
assert len(cassette_response.history) == len(response.history)
assert len(cassette) == 1
assert cassette.play_count == 1
def test_double_requests(tmpdir, mockbin_request_url):
"""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 = "https://httpbin.org/get" url = mockbin_request_url
with vcr.use_cassette(str(tmpdir.join("text.yaml"))): with vcr.use_cassette(str(tmpdir.join("text.yaml"))):
_, response_text1 = get(url, output="text") _, response_text1 = get(url, output="text")
@@ -302,3 +318,99 @@ def test_double_requests(tmpdir):
# Now that we made both requests, we should have played both. # Now that we made both requests, we should have played both.
assert cassette.play_count == 2 assert cassette.play_count == 2
def test_cookies(scheme, tmpdir):
async def run(loop):
cookies_url = scheme + (
"://httpbin.org/response-headers?"
"set-cookie=" + urllib.parse.quote("cookie_1=val_1; Path=/") + "&"
"Set-Cookie=" + urllib.parse.quote("Cookie_2=Val_2; Path=/")
)
home_url = scheme + "://httpbin.org/"
tmp = str(tmpdir.join("cookies.yaml"))
req_cookies = {"Cookie_3": "Val_3"}
req_headers = {"Cookie": "Cookie_4=Val_4"}
# ------------------------- Record -------------------------- #
with vcr.use_cassette(tmp) as cassette:
async with aiohttp.ClientSession(loop=loop) as session:
cookies_resp = await session.get(cookies_url)
home_resp = await session.get(home_url, cookies=req_cookies, headers=req_headers)
assert cassette.play_count == 0
assert_responses(cookies_resp, home_resp)
# -------------------------- Play --------------------------- #
with vcr.use_cassette(tmp, record_mode=vcr.mode.NONE) as cassette:
async with aiohttp.ClientSession(loop=loop) as session:
cookies_resp = await session.get(cookies_url)
home_resp = await session.get(home_url, cookies=req_cookies, headers=req_headers)
assert cassette.play_count == 2
assert_responses(cookies_resp, home_resp)
def assert_responses(cookies_resp, home_resp):
assert cookies_resp.cookies.get("cookie_1").value == "val_1"
assert cookies_resp.cookies.get("Cookie_2").value == "Val_2"
request_cookies = home_resp.request_info.headers["cookie"]
assert "cookie_1=val_1" in request_cookies
assert "Cookie_2=Val_2" in request_cookies
assert "Cookie_3=Val_3" in request_cookies
assert "Cookie_4=Val_4" in request_cookies
run_in_loop(run)
def test_cookies_redirect(scheme, tmpdir):
async def run(loop):
# Sets cookie as provided by the query string and redirects
cookies_url = scheme + "://httpbin.org/cookies/set?Cookie_1=Val_1"
tmp = str(tmpdir.join("cookies.yaml"))
# ------------------------- Record -------------------------- #
with vcr.use_cassette(tmp) as cassette:
async with aiohttp.ClientSession(loop=loop) as session:
cookies_resp = await session.get(cookies_url)
assert not cookies_resp.cookies
cookies = session.cookie_jar.filter_cookies(cookies_url)
assert cookies["Cookie_1"].value == "Val_1"
assert cassette.play_count == 0
cassette.requests[1].headers["Cookie"] == "Cookie_1=Val_1"
# -------------------------- Play --------------------------- #
with vcr.use_cassette(tmp, record_mode=vcr.mode.NONE) as cassette:
async with aiohttp.ClientSession(loop=loop) as session:
cookies_resp = await session.get(cookies_url)
assert not cookies_resp.cookies
cookies = session.cookie_jar.filter_cookies(cookies_url)
assert cookies["Cookie_1"].value == "Val_1"
assert cassette.play_count == 2
cassette.requests[1].headers["Cookie"] == "Cookie_1=Val_1"
# Assert that it's ignoring expiration date
with vcr.use_cassette(tmp, record_mode=vcr.mode.NONE) as cassette:
cassette.responses[0]["headers"]["set-cookie"] = [
"Cookie_1=Val_1; Expires=Wed, 21 Oct 2015 07:28:00 GMT"
]
async with aiohttp.ClientSession(loop=loop) as session:
cookies_resp = await session.get(cookies_url)
assert not cookies_resp.cookies
cookies = session.cookie_jar.filter_cookies(cookies_url)
assert cookies["Cookie_1"].value == "Val_1"
run_in_loop(run)
def test_not_allow_redirects(tmpdir, mockbin):
url = mockbin + "/redirect/308/5"
path = str(tmpdir.join("redirects.yaml"))
with vcr.use_cassette(path):
response, _ = get(url, allow_redirects=False)
assert response.url.path == "/redirect/308/5"
assert response.status == 308
with vcr.use_cassette(path) as cassette:
response, _ = get(url, allow_redirects=False)
assert response.url.path == "/redirect/308/5"
assert response.status == 308
assert cassette.play_count == 1

View File

@@ -11,7 +11,7 @@ import vcr
def test_nonexistent_directory(tmpdir, httpbin): def test_nonexistent_directory(tmpdir, httpbin):
"""If we load a cassette in a nonexistent directory, it can save ok""" """If we load a cassette in a nonexistent directory, it can save ok"""
# Check to make sure directory doesnt exist # Check to make sure directory doesn't exist
assert not os.path.exists(str(tmpdir.join("nonexistent"))) assert not os.path.exists(str(tmpdir.join("nonexistent")))
# Run VCR to create dir and cassette file # Run VCR to create dir and cassette file

View File

@@ -2,11 +2,13 @@ import pytest
boto = pytest.importorskip("boto") boto = pytest.importorskip("boto")
from configparser import DuplicateSectionError # NOQA
import boto # NOQA import boto # NOQA
import boto.iam # NOQA import boto.iam # NOQA
from boto.s3.connection import S3Connection # NOQA from boto.s3.connection import S3Connection # NOQA
from boto.s3.key import Key # NOQA from boto.s3.key import Key # NOQA
from configparser import DuplicateSectionError # NOQA
import vcr # NOQA import vcr # NOQA
@@ -15,6 +17,7 @@ def test_boto_stubs(tmpdir):
# Perform the imports within the patched context so that # Perform the imports within the patched context so that
# CertValidatingHTTPSConnection refers to the patched version. # CertValidatingHTTPSConnection refers to the patched version.
from boto.https_connection import CertValidatingHTTPSConnection from boto.https_connection import CertValidatingHTTPSConnection
from vcr.stubs.boto_stubs import VCRCertValidatingHTTPSConnection from vcr.stubs.boto_stubs import VCRCertValidatingHTTPSConnection
# Prove that the class was patched by the stub and that we can instantiate it. # Prove that the class was patched by the stub and that we can instantiate it.

View File

@@ -1,10 +1,12 @@
import pytest
import os import os
import pytest
boto3 = pytest.importorskip("boto3") boto3 = pytest.importorskip("boto3")
import boto3 # NOQA import boto3 # NOQA
import botocore # NOQA import botocore # NOQA
import vcr # NOQA import vcr # NOQA
try: try:
@@ -55,24 +57,6 @@ def get_user(iam_client):
return _get_user return _get_user
@boto3_skip_vendored_requests
def test_boto_vendored_stubs(tmpdir):
with vcr.use_cassette(str(tmpdir.join("boto3-stubs.yml"))):
# Perform the imports within the patched context so that
# HTTPConnection, VerifiedHTTPSConnection refers to the patched version.
from botocore.vendored.requests.packages.urllib3.connectionpool import (
HTTPConnection,
VerifiedHTTPSConnection,
)
from vcr.stubs.boto3_stubs import VCRRequestsHTTPConnection, VCRRequestsHTTPSConnection
# Prove that the class was patched by the stub and that we can instantiate it.
assert issubclass(HTTPConnection, VCRRequestsHTTPConnection)
assert issubclass(VerifiedHTTPSConnection, VCRRequestsHTTPSConnection)
HTTPConnection("hostname.does.not.matter")
VerifiedHTTPSConnection("hostname.does.not.matter")
@pytest.mark.skipif( @pytest.mark.skipif(
os.environ.get("TRAVIS_PULL_REQUEST") != "false", os.environ.get("TRAVIS_PULL_REQUEST") != "false",
reason="Encrypted Environment Variables from Travis Repository Settings" reason="Encrypted Environment Variables from Travis Repository Settings"
@@ -80,7 +64,6 @@ def test_boto_vendored_stubs(tmpdir):
"https://docs.travis-ci.com/user/pull-requests/#pull-requests-and-security-restrictions", "https://docs.travis-ci.com/user/pull-requests/#pull-requests-and-security-restrictions",
) )
def test_boto_medium_difficulty(tmpdir, get_user): def test_boto_medium_difficulty(tmpdir, get_user):
with vcr.use_cassette(str(tmpdir.join("boto3-medium.yml"))): with vcr.use_cassette(str(tmpdir.join("boto3-medium.yml"))):
response = get_user() response = get_user()
assert response["User"]["UserName"] == IAM_USER_NAME assert response["User"]["UserName"] == IAM_USER_NAME

View File

@@ -1,50 +1,54 @@
import os
import json import json
import pytest import os
import vcr
from urllib.request import urlopen from urllib.request import urlopen
import pytest
def test_set_serializer_default_config(tmpdir, httpbin): import vcr
def test_set_serializer_default_config(tmpdir, mockbin_request_url):
my_vcr = vcr.VCR(serializer="json") my_vcr = vcr.VCR(serializer="json")
with my_vcr.use_cassette(str(tmpdir.join("test.json"))): with my_vcr.use_cassette(str(tmpdir.join("test.json"))):
assert my_vcr.serializer == "json" assert my_vcr.serializer == "json"
urlopen(httpbin.url + "/get") urlopen(mockbin_request_url)
with open(str(tmpdir.join("test.json"))) as f: with open(str(tmpdir.join("test.json"))) as f:
assert json.loads(f.read()) file_content = f.read()
assert file_content.endswith("\n")
assert json.loads(file_content)
def test_default_set_cassette_library_dir(tmpdir, httpbin): def test_default_set_cassette_library_dir(tmpdir, mockbin_request_url):
my_vcr = vcr.VCR(cassette_library_dir=str(tmpdir.join("subdir"))) my_vcr = vcr.VCR(cassette_library_dir=str(tmpdir.join("subdir")))
with my_vcr.use_cassette("test.json"): with my_vcr.use_cassette("test.json"):
urlopen(httpbin.url + "/get") urlopen(mockbin_request_url)
assert os.path.exists(str(tmpdir.join("subdir").join("test.json"))) assert os.path.exists(str(tmpdir.join("subdir").join("test.json")))
def test_override_set_cassette_library_dir(tmpdir, httpbin): def test_override_set_cassette_library_dir(tmpdir, mockbin_request_url):
my_vcr = vcr.VCR(cassette_library_dir=str(tmpdir.join("subdir"))) my_vcr = vcr.VCR(cassette_library_dir=str(tmpdir.join("subdir")))
cld = str(tmpdir.join("subdir2")) cld = str(tmpdir.join("subdir2"))
with my_vcr.use_cassette("test.json", cassette_library_dir=cld): with my_vcr.use_cassette("test.json", cassette_library_dir=cld):
urlopen(httpbin.url + "/get") urlopen(mockbin_request_url)
assert os.path.exists(str(tmpdir.join("subdir2").join("test.json"))) assert os.path.exists(str(tmpdir.join("subdir2").join("test.json")))
assert not os.path.exists(str(tmpdir.join("subdir").join("test.json"))) assert not os.path.exists(str(tmpdir.join("subdir").join("test.json")))
def test_override_match_on(tmpdir, httpbin): def test_override_match_on(tmpdir, mockbin_request_url):
my_vcr = vcr.VCR(match_on=["method"]) my_vcr = vcr.VCR(match_on=["method"])
with my_vcr.use_cassette(str(tmpdir.join("test.json"))): with my_vcr.use_cassette(str(tmpdir.join("test.json"))):
urlopen(httpbin.url) urlopen(mockbin_request_url)
with my_vcr.use_cassette(str(tmpdir.join("test.json"))) as cass: with my_vcr.use_cassette(str(tmpdir.join("test.json"))) as cass:
urlopen(httpbin.url + "/get") urlopen(mockbin_request_url)
assert len(cass) == 1 assert len(cass) == 1
assert cass.play_count == 1 assert cass.play_count == 1
@@ -56,3 +60,23 @@ def test_missing_matcher():
with pytest.raises(KeyError): with pytest.raises(KeyError):
with my_vcr.use_cassette("test.yaml", match_on=["notawesome"]): with my_vcr.use_cassette("test.yaml", match_on=["notawesome"]):
pass pass
def test_dont_record_on_exception(tmpdir, mockbin_request_url):
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)
with pytest.raises(AssertionError):
some_test()
assert not os.path.exists(str(tmpdir.join("dontsave.yml")))
# 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 not os.path.exists(str(tmpdir.join("dontsave2.yml")))

View File

@@ -10,19 +10,19 @@ from urllib.request import urlopen
import vcr import vcr
def test_disk_saver_nowrite(tmpdir, httpbin): def test_disk_saver_nowrite(tmpdir, mockbin_request_url):
""" """
Ensure that when you close a cassette without changing it it doesn't Ensure that when you close a cassette without changing it it doesn't
rewrite the file rewrite the file
""" """
fname = str(tmpdir.join("synopsis.yaml")) fname = str(tmpdir.join("synopsis.yaml"))
with vcr.use_cassette(fname) as cass: with vcr.use_cassette(fname) as cass:
urlopen(httpbin.url).read() urlopen(mockbin_request_url).read()
assert cass.play_count == 0 assert cass.play_count == 0
last_mod = os.path.getmtime(fname) last_mod = os.path.getmtime(fname)
with vcr.use_cassette(fname) as cass: with vcr.use_cassette(fname) as cass:
urlopen(httpbin.url).read() urlopen(mockbin_request_url).read()
assert cass.play_count == 1 assert cass.play_count == 1
assert cass.dirty is False assert cass.dirty is False
last_mod2 = os.path.getmtime(fname) last_mod2 = os.path.getmtime(fname)
@@ -30,14 +30,14 @@ def test_disk_saver_nowrite(tmpdir, httpbin):
assert last_mod == last_mod2 assert last_mod == last_mod2
def test_disk_saver_write(tmpdir, httpbin): def test_disk_saver_write(tmpdir, mockbin_request_url):
""" """
Ensure that when you close a cassette after changing it it does Ensure that when you close a cassette after changing it it does
rewrite the file rewrite the file
""" """
fname = str(tmpdir.join("synopsis.yaml")) fname = str(tmpdir.join("synopsis.yaml"))
with vcr.use_cassette(fname) as cass: with vcr.use_cassette(fname) as cass:
urlopen(httpbin.url).read() urlopen(mockbin_request_url).read()
assert cass.play_count == 0 assert cass.play_count == 0
last_mod = os.path.getmtime(fname) last_mod = os.path.getmtime(fname)
@@ -45,9 +45,9 @@ def test_disk_saver_write(tmpdir, httpbin):
# the mtime doesn't change # the mtime doesn't change
time.sleep(1) time.sleep(1)
with vcr.use_cassette(fname, record_mode="any") as cass: with vcr.use_cassette(fname, record_mode=vcr.mode.ANY) as cass:
urlopen(httpbin.url).read() urlopen(mockbin_request_url).read()
urlopen(httpbin.url + "/get").read() urlopen(mockbin_request_url + "/get").read()
assert cass.play_count == 1 assert cass.play_count == 1
assert cass.dirty assert cass.dirty
last_mod2 = os.path.getmtime(fname) last_mod2 = os.path.getmtime(fname)

View File

@@ -1,12 +1,14 @@
import base64 import base64
import pytest
from urllib.request import urlopen, Request
from urllib.parse import urlencode
from urllib.error import HTTPError
import vcr
import json import json
from urllib.error import HTTPError
from urllib.parse import urlencode
from urllib.request import Request, urlopen
import pytest
from assertions import assert_cassette_has_one_response, assert_is_json from assertions import assert_cassette_has_one_response, assert_is_json
import vcr
def _request_with_auth(url, username, password): def _request_with_auth(url, username, password):
request = Request(url) request = Request(url)
@@ -106,6 +108,18 @@ def test_decompress_gzip(tmpdir, httpbin):
assert_is_json(decoded_response) assert_is_json(decoded_response)
def test_decomptess_empty_body(tmpdir, httpbin):
url = httpbin.url + "/gzip"
request = Request(url, headers={"Accept-Encoding": ["gzip, deflate"]}, method="HEAD")
cass_file = str(tmpdir.join("gzip_empty_response.yaml"))
with vcr.use_cassette(cass_file, decode_compressed_response=True):
response = urlopen(request).read()
with vcr.use_cassette(cass_file) as cass:
decoded_response = urlopen(request).read()
assert_cassette_has_one_response(cass)
assert decoded_response == response
def test_decompress_deflate(tmpdir, httpbin): def test_decompress_deflate(tmpdir, httpbin):
url = httpbin.url + "/deflate" url = httpbin.url + "/deflate"
request = Request(url, headers={"Accept-Encoding": ["gzip, deflate"]}) request = Request(url, headers={"Accept-Encoding": ["gzip, deflate"]})

View File

@@ -1,16 +1,13 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
"""Integration tests with httplib2""" """Integration tests with httplib2"""
import sys
from urllib.parse import urlencode from urllib.parse import urlencode
import pytest import pytest
import pytest_httpbin.certs import pytest_httpbin.certs
from assertions import assert_cassette_has_one_response
import vcr import vcr
from assertions import assert_cassette_has_one_response
httplib2 = pytest.importorskip("httplib2") httplib2 = pytest.importorskip("httplib2")
@@ -20,8 +17,6 @@ def http():
with the certificate replaced by the httpbin one. with the certificate replaced by the httpbin one.
""" """
kwargs = {"ca_certs": pytest_httpbin.certs.where()} kwargs = {"ca_certs": pytest_httpbin.certs.where()}
if sys.version_info[:2] in [(2, 7), (3, 7)]:
kwargs["disable_ssl_certificate_validation"] = True
return httplib2.Http(**kwargs) return httplib2.Http(**kwargs)
@@ -61,13 +56,14 @@ def test_response_headers(tmpdir, httpbin_both):
assert set(headers) == set(resp.items()) assert set(headers) == set(resp.items())
def test_effective_url(tmpdir, httpbin_both): def test_effective_url(tmpdir):
"""Ensure that the effective_url is captured""" """Ensure that the effective_url is captured"""
url = httpbin_both.url + "/redirect-to?url=/html" url = "http://mockbin.org/redirect/301"
with vcr.use_cassette(str(tmpdir.join("headers.yaml"))): with vcr.use_cassette(str(tmpdir.join("headers.yaml"))):
resp, _ = http().request(url) resp, _ = http().request(url)
effective_url = resp["content-location"] effective_url = resp["content-location"]
assert effective_url == httpbin_both + "/html" assert effective_url == "http://mockbin.org/redirect/301/0"
with vcr.use_cassette(str(tmpdir.join("headers.yaml"))): with vcr.use_cassette(str(tmpdir.join("headers.yaml"))):
resp, _ = http().request(url) resp, _ = http().request(url)

View File

@@ -0,0 +1,306 @@
import os
import pytest
asyncio = pytest.importorskip("asyncio")
httpx = pytest.importorskip("httpx")
import vcr # noqa: E402
from vcr.stubs.httpx_stubs import HTTPX_REDIRECT_PARAM # noqa: E402
class BaseDoRequest:
_client_class = None
def __init__(self, *args, **kwargs):
self._client_args = args
self._client_kwargs = kwargs
def _make_client(self):
return self._client_class(*self._client_args, **self._client_kwargs)
class DoSyncRequest(BaseDoRequest):
_client_class = httpx.Client
def __enter__(self):
return self
def __exit__(self, *args):
pass
@property
def client(self):
try:
return self._client
except AttributeError:
self._client = self._make_client()
return self._client
def __call__(self, *args, **kwargs):
return self.client.request(*args, timeout=60, **kwargs)
class DoAsyncRequest(BaseDoRequest):
_client_class = httpx.AsyncClient
def __enter__(self):
# Need to manage both loop and client, because client's implementation
# will fail if the loop is closed before the client's end of life.
self._loop = asyncio.new_event_loop()
asyncio.set_event_loop(self._loop)
self._client = self._make_client()
self._loop.run_until_complete(self._client.__aenter__())
return self
def __exit__(self, *args):
try:
self._loop.run_until_complete(self._client.__aexit__(*args))
finally:
del self._client
self._loop.close()
del self._loop
@property
def client(self):
try:
return self._client
except AttributeError:
raise ValueError('To access async client, use "with do_request() as client"')
def __call__(self, *args, **kwargs):
if hasattr(self, "_loop"):
return self._loop.run_until_complete(self.client.request(*args, **kwargs))
# Use one-time context and dispose of the loop/client afterwards
with self:
return self(*args, **kwargs)
def pytest_generate_tests(metafunc):
if "do_request" in metafunc.fixturenames:
metafunc.parametrize("do_request", [DoAsyncRequest, DoSyncRequest])
@pytest.fixture
def yml(tmpdir, request):
return str(tmpdir.join(request.function.__name__ + ".yaml"))
def test_status(tmpdir, mockbin, do_request):
url = mockbin
with vcr.use_cassette(str(tmpdir.join("status.yaml"))):
response = do_request()("GET", url)
with vcr.use_cassette(str(tmpdir.join("status.yaml"))) as cassette:
cassette_response = do_request()("GET", url)
assert cassette_response.status_code == response.status_code
assert cassette.play_count == 1
def test_case_insensitive_headers(tmpdir, mockbin, do_request):
url = mockbin
with vcr.use_cassette(str(tmpdir.join("whatever.yaml"))):
do_request()("GET", url)
with vcr.use_cassette(str(tmpdir.join("whatever.yaml"))) as cassette:
cassette_response = do_request()("GET", url)
assert "Content-Type" in cassette_response.headers
assert "content-type" in cassette_response.headers
assert cassette.play_count == 1
def test_content(tmpdir, mockbin, do_request):
url = mockbin
with vcr.use_cassette(str(tmpdir.join("cointent.yaml"))):
response = do_request()("GET", url)
with vcr.use_cassette(str(tmpdir.join("cointent.yaml"))) as cassette:
cassette_response = do_request()("GET", url)
assert cassette_response.content == response.content
assert cassette.play_count == 1
def test_json(tmpdir, mockbin, do_request):
url = mockbin + "/request"
headers = {"content-type": "application/json"}
with vcr.use_cassette(str(tmpdir.join("json.yaml"))):
response = do_request(headers=headers)("GET", url)
with vcr.use_cassette(str(tmpdir.join("json.yaml"))) as cassette:
cassette_response = do_request(headers=headers)("GET", url)
assert cassette_response.json() == response.json()
assert cassette.play_count == 1
def test_params_same_url_distinct_params(tmpdir, mockbin, do_request):
url = mockbin + "/request"
headers = {"Content-Type": "application/json"}
params = {"a": 1, "b": False, "c": "c"}
with vcr.use_cassette(str(tmpdir.join("get.yaml"))) as cassette:
response = do_request()("GET", url, params=params, headers=headers)
with vcr.use_cassette(str(tmpdir.join("get.yaml"))) as cassette:
cassette_response = do_request()("GET", url, params=params, headers=headers)
assert cassette_response.request.url == response.request.url
assert cassette_response.json() == response.json()
assert cassette.play_count == 1
params = {"other": "params"}
with vcr.use_cassette(str(tmpdir.join("get.yaml"))) as cassette:
with pytest.raises(vcr.errors.CannotOverwriteExistingCassetteException):
do_request()("GET", url, params=params, headers=headers)
def test_redirect(mockbin, yml, do_request):
url = mockbin + "/redirect/303/2"
redirect_kwargs = {HTTPX_REDIRECT_PARAM.name: True}
response = do_request()("GET", url, **redirect_kwargs)
with vcr.use_cassette(yml):
response = do_request()("GET", url, **redirect_kwargs)
with vcr.use_cassette(yml) as cassette:
cassette_response = do_request()("GET", url, **redirect_kwargs)
assert cassette_response.status_code == response.status_code
assert len(cassette_response.history) == len(response.history)
assert len(cassette) == 3
assert cassette.play_count == 3
# Assert that the real response and the cassette response have a similar
# looking request_info.
assert cassette_response.request.url == response.request.url
assert cassette_response.request.method == response.request.method
assert {k: v for k, v in cassette_response.request.headers.items()} == {
k: v for k, v in response.request.headers.items()
}
def test_work_with_gzipped_data(mockbin, do_request, yml):
url = mockbin + "/gzip?foo=bar"
headers = {"accept-encoding": "deflate, gzip"}
with vcr.use_cassette(yml):
do_request(headers=headers)("GET", url)
with vcr.use_cassette(yml) as cassette:
cassette_response = do_request(headers=headers)("GET", url)
assert cassette_response.headers["content-encoding"] == "gzip"
assert cassette_response.read()
assert cassette.play_count == 1
@pytest.mark.parametrize("url", ["https://github.com/kevin1024/vcrpy/issues/" + str(i) for i in range(3, 6)])
def test_simple_fetching(do_request, yml, url):
with vcr.use_cassette(yml):
do_request()("GET", url)
with vcr.use_cassette(yml) as cassette:
cassette_response = do_request()("GET", url)
assert str(cassette_response.request.url) == url
assert cassette.play_count == 1
def test_behind_proxy(do_request):
# This is recorded because otherwise we should have a live proxy somewhere.
yml = (
os.path.dirname(os.path.realpath(__file__)) + "/cassettes/" + "test_httpx_test_test_behind_proxy.yml"
)
url = "https://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
def test_cookies(tmpdir, mockbin, do_request):
def client_cookies(client):
return [c for c in client.client.cookies]
def response_cookies(response):
return [c for c in response.cookies]
url = mockbin + "/bin/26148652-fe25-4f21-aaf5-689b5b4bf65f"
headers = {"cookie": "k1=v1;k2=v2"}
with do_request(headers=headers) 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)
assert response_cookies(r1) == ["k1", "k2"]
r2 = client("GET", url, **redirect_kwargs)
assert response_cookies(r2) == ["k1", "k2"]
assert client_cookies(client) == ["k1", "k2"]
with do_request(headers=headers) as new_client:
assert client_cookies(new_client) == []
with vcr.use_cassette(testfile) as cassette:
cassette_response = new_client("GET", url)
assert cassette.play_count == 1
assert response_cookies(cassette_response) == ["k1", "k2"]
assert client_cookies(new_client) == ["k1", "k2"]
def test_relative_redirects(tmpdir, scheme, do_request, mockbin):
redirect_kwargs = {HTTPX_REDIRECT_PARAM.name: True}
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")
with vcr.use_cassette(testfile) as cassette:
response = do_request()("GET", url, **redirect_kwargs)
assert len(response.history) == 2
assert response.json()["url"].endswith("request")
assert cassette.play_count == 3
def test_redirect_wo_allow_redirects(do_request, 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
with vcr.use_cassette(yml) as cassette:
response = do_request()("GET", url, **redirect_kwargs)
assert str(response.url).endswith("308/5")
assert response.status_code == 308
assert cassette.play_count == 1

View File

@@ -1,6 +1,7 @@
from urllib.request import urlopen
import socket import socket
from contextlib import contextmanager from contextlib import contextmanager
from urllib.request import urlopen
import vcr import vcr

View File

@@ -1,7 +1,8 @@
import vcr
import pytest
from urllib.request import urlopen from urllib.request import urlopen
import pytest
import vcr
DEFAULT_URI = "http://httpbin.org/get?p1=q1&p2=q2" # base uri for testing DEFAULT_URI = "http://httpbin.org/get?p1=q1&p2=q2" # base uri for testing
@@ -13,13 +14,13 @@ def _replace_httpbin(uri, httpbin, httpbin_secure):
@pytest.fixture @pytest.fixture
def cassette(tmpdir, httpbin, httpbin_secure): def cassette(tmpdir, httpbin, httpbin_secure):
""" """
Helper fixture used to prepare the cassete Helper fixture used to prepare the cassette
returns path to the recorded cassette returns path to the recorded cassette
""" """
default_uri = _replace_httpbin(DEFAULT_URI, httpbin, httpbin_secure) default_uri = _replace_httpbin(DEFAULT_URI, httpbin, httpbin_secure)
cassette_path = str(tmpdir.join("test.yml")) cassette_path = str(tmpdir.join("test.yml"))
with vcr.use_cassette(cassette_path, record_mode="all"): with vcr.use_cassette(cassette_path, record_mode=vcr.mode.ALL):
urlopen(default_uri) urlopen(default_uri)
return cassette_path return cassette_path
@@ -35,7 +36,6 @@ def cassette(tmpdir, httpbin, httpbin_secure):
], ],
) )
def test_matchers(httpbin, httpbin_secure, cassette, matcher, matching_uri, not_matching_uri): def test_matchers(httpbin, httpbin_secure, cassette, matcher, matching_uri, not_matching_uri):
matching_uri = _replace_httpbin(matching_uri, httpbin, httpbin_secure) matching_uri = _replace_httpbin(matching_uri, httpbin, httpbin_secure)
not_matching_uri = _replace_httpbin(not_matching_uri, httpbin, httpbin_secure) not_matching_uri = _replace_httpbin(not_matching_uri, httpbin, httpbin_secure)
default_uri = _replace_httpbin(DEFAULT_URI, httpbin, httpbin_secure) default_uri = _replace_httpbin(DEFAULT_URI, httpbin, httpbin_secure)
@@ -75,7 +75,6 @@ def test_method_matcher(cassette, httpbin, httpbin_secure):
"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): def test_default_matcher_matches(cassette, uri, httpbin, httpbin_secure):
uri = _replace_httpbin(uri, httpbin, httpbin_secure) uri = _replace_httpbin(uri, httpbin, httpbin_secure)
with vcr.use_cassette(cassette) as cass: with vcr.use_cassette(cassette) as cass:

View File

@@ -1,7 +1,9 @@
import pytest
import vcr
from urllib.request import urlopen from urllib.request import urlopen
import pytest
import vcr
def test_making_extra_request_raises_exception(tmpdir, httpbin): def test_making_extra_request_raises_exception(tmpdir, httpbin):
# make two requests in the first request that are considered # make two requests in the first request that are considered

View File

@@ -1,15 +1,13 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
"""Test using a proxy.""" """Test using a proxy."""
# External imports
import multiprocessing
import pytest
import http.server import http.server
import multiprocessing
import socketserver import socketserver
from urllib.request import urlopen from urllib.request import urlopen
# Internal imports import pytest
import vcr import vcr
# Conditional imports # Conditional imports
@@ -39,7 +37,7 @@ class Proxy(http.server.SimpleHTTPRequestHandler):
self.copyfile(upstream_response, self.wfile) self.copyfile(upstream_response, self.wfile)
@pytest.yield_fixture(scope="session") @pytest.fixture(scope="session")
def proxy_server(): def proxy_server():
httpd = socketserver.ThreadingTCPServer(("", 0), Proxy) httpd = socketserver.ThreadingTCPServer(("", 0), Proxy)
proxy_process = multiprocessing.Process(target=httpd.serve_forever) proxy_process = multiprocessing.Process(target=httpd.serve_forever)

View File

@@ -1,15 +1,17 @@
import pytest
import vcr
from urllib.request import urlopen from urllib.request import urlopen
import pytest
import vcr
def test_once_record_mode(tmpdir, httpbin): def test_once_record_mode(tmpdir, httpbin):
testfile = str(tmpdir.join("recordmode.yml")) testfile = str(tmpdir.join("recordmode.yml"))
with vcr.use_cassette(testfile, record_mode="once"): with vcr.use_cassette(testfile, record_mode=vcr.mode.ONCE):
# cassette file doesn't exist, so create. # cassette file doesn't exist, so create.
urlopen(httpbin.url).read() urlopen(httpbin.url).read()
with vcr.use_cassette(testfile, record_mode="once"): with vcr.use_cassette(testfile, record_mode=vcr.mode.ONCE):
# make the same request again # make the same request again
urlopen(httpbin.url).read() urlopen(httpbin.url).read()
@@ -22,12 +24,12 @@ def test_once_record_mode(tmpdir, httpbin):
def test_once_record_mode_two_times(tmpdir, httpbin): def test_once_record_mode_two_times(tmpdir, httpbin):
testfile = str(tmpdir.join("recordmode.yml")) testfile = str(tmpdir.join("recordmode.yml"))
with vcr.use_cassette(testfile, record_mode="once"): with vcr.use_cassette(testfile, record_mode=vcr.mode.ONCE):
# get two of the same file # get two of the same file
urlopen(httpbin.url).read() urlopen(httpbin.url).read()
urlopen(httpbin.url).read() urlopen(httpbin.url).read()
with vcr.use_cassette(testfile, record_mode="once"): with vcr.use_cassette(testfile, record_mode=vcr.mode.ONCE):
# do it again # do it again
urlopen(httpbin.url).read() urlopen(httpbin.url).read()
urlopen(httpbin.url).read() urlopen(httpbin.url).read()
@@ -35,7 +37,7 @@ def test_once_record_mode_two_times(tmpdir, httpbin):
def test_once_mode_three_times(tmpdir, httpbin): def test_once_mode_three_times(tmpdir, httpbin):
testfile = str(tmpdir.join("recordmode.yml")) testfile = str(tmpdir.join("recordmode.yml"))
with vcr.use_cassette(testfile, record_mode="once"): with vcr.use_cassette(testfile, record_mode=vcr.mode.ONCE):
# get three of the same file # get three of the same file
urlopen(httpbin.url).read() urlopen(httpbin.url).read()
urlopen(httpbin.url).read() urlopen(httpbin.url).read()
@@ -45,11 +47,11 @@ def test_once_mode_three_times(tmpdir, httpbin):
def test_new_episodes_record_mode(tmpdir, httpbin): def test_new_episodes_record_mode(tmpdir, httpbin):
testfile = str(tmpdir.join("recordmode.yml")) testfile = str(tmpdir.join("recordmode.yml"))
with vcr.use_cassette(testfile, record_mode="new_episodes"): with vcr.use_cassette(testfile, record_mode=vcr.mode.NEW_EPISODES):
# cassette file doesn't exist, so create. # cassette file doesn't exist, so create.
urlopen(httpbin.url).read() urlopen(httpbin.url).read()
with vcr.use_cassette(testfile, record_mode="new_episodes") as cass: with vcr.use_cassette(testfile, record_mode=vcr.mode.NEW_EPISODES) as cass:
# make the same request again # make the same request again
urlopen(httpbin.url).read() urlopen(httpbin.url).read()
@@ -57,7 +59,7 @@ def test_new_episodes_record_mode(tmpdir, httpbin):
assert cass.all_played assert cass.all_played
# in the "new_episodes" record mode, we can add more requests to # in the "new_episodes" record mode, we can add more requests to
# a cassette without repurcussions. # a cassette without repercussions.
urlopen(httpbin.url + "/get").read() urlopen(httpbin.url + "/get").read()
# one of the responses has been played # one of the responses has been played
@@ -66,7 +68,7 @@ def test_new_episodes_record_mode(tmpdir, httpbin):
# not all responses have been played # not all responses have been played
assert not cass.all_played assert not cass.all_played
with vcr.use_cassette(testfile, record_mode="new_episodes") as cass: with vcr.use_cassette(testfile, record_mode=vcr.mode.NEW_EPISODES) as cass:
# the cassette should now have 2 responses # the cassette should now have 2 responses
assert len(cass.responses) == 2 assert len(cass.responses) == 2
@@ -74,11 +76,11 @@ def test_new_episodes_record_mode(tmpdir, httpbin):
def test_new_episodes_record_mode_two_times(tmpdir, httpbin): def test_new_episodes_record_mode_two_times(tmpdir, httpbin):
testfile = str(tmpdir.join("recordmode.yml")) testfile = str(tmpdir.join("recordmode.yml"))
url = httpbin.url + "/bytes/1024" url = httpbin.url + "/bytes/1024"
with vcr.use_cassette(testfile, record_mode="new_episodes"): with vcr.use_cassette(testfile, record_mode=vcr.mode.NEW_EPISODES):
# cassette file doesn't exist, so create. # cassette file doesn't exist, so create.
original_first_response = urlopen(url).read() original_first_response = urlopen(url).read()
with vcr.use_cassette(testfile, record_mode="new_episodes"): with vcr.use_cassette(testfile, record_mode=vcr.mode.NEW_EPISODES):
# make the same request again # make the same request again
assert urlopen(url).read() == original_first_response assert urlopen(url).read() == original_first_response
@@ -86,7 +88,7 @@ def test_new_episodes_record_mode_two_times(tmpdir, httpbin):
# to the cassette without repercussions # to the cassette without repercussions
original_second_response = urlopen(url).read() original_second_response = urlopen(url).read()
with vcr.use_cassette(testfile, record_mode="once"): with vcr.use_cassette(testfile, record_mode=vcr.mode.ONCE):
# make the same request again # make the same request again
assert urlopen(url).read() == original_first_response assert urlopen(url).read() == original_first_response
assert urlopen(url).read() == original_second_response assert urlopen(url).read() == original_second_response
@@ -99,16 +101,16 @@ def test_new_episodes_record_mode_two_times(tmpdir, httpbin):
def test_all_record_mode(tmpdir, httpbin): def test_all_record_mode(tmpdir, httpbin):
testfile = str(tmpdir.join("recordmode.yml")) testfile = str(tmpdir.join("recordmode.yml"))
with vcr.use_cassette(testfile, record_mode="all"): with vcr.use_cassette(testfile, record_mode=vcr.mode.ALL):
# cassette file doesn't exist, so create. # cassette file doesn't exist, so create.
urlopen(httpbin.url).read() urlopen(httpbin.url).read()
with vcr.use_cassette(testfile, record_mode="all") as cass: with vcr.use_cassette(testfile, record_mode=vcr.mode.ALL) as cass:
# make the same request again # make the same request again
urlopen(httpbin.url).read() urlopen(httpbin.url).read()
# in the "all" record mode, we can add more requests to # in the "all" record mode, we can add more requests to
# a cassette without repurcussions. # a cassette without repercussions.
urlopen(httpbin.url + "/get").read() urlopen(httpbin.url + "/get").read()
# The cassette was never actually played, even though it existed. # The cassette was never actually played, even though it existed.
@@ -121,7 +123,7 @@ def test_none_record_mode(tmpdir, httpbin):
# Cassette file doesn't exist, yet we are trying to make a request. # Cassette file doesn't exist, yet we are trying to make a request.
# raise hell. # raise hell.
testfile = str(tmpdir.join("recordmode.yml")) testfile = str(tmpdir.join("recordmode.yml"))
with vcr.use_cassette(testfile, record_mode="none"): with vcr.use_cassette(testfile, record_mode=vcr.mode.NONE):
with pytest.raises(Exception): with pytest.raises(Exception):
urlopen(httpbin.url).read() urlopen(httpbin.url).read()
@@ -130,11 +132,11 @@ def test_none_record_mode_with_existing_cassette(tmpdir, httpbin):
# create a cassette file # create a cassette file
testfile = str(tmpdir.join("recordmode.yml")) testfile = str(tmpdir.join("recordmode.yml"))
with vcr.use_cassette(testfile, record_mode="all"): with vcr.use_cassette(testfile, record_mode=vcr.mode.ALL):
urlopen(httpbin.url).read() urlopen(httpbin.url).read()
# play from cassette file # play from cassette file
with vcr.use_cassette(testfile, record_mode="none") as cass: with vcr.use_cassette(testfile, record_mode=vcr.mode.NONE) as cass:
urlopen(httpbin.url).read() urlopen(httpbin.url).read()
assert cass.play_count == 1 assert cass.play_count == 1
# but if I try to hit the net, raise an exception. # but if I try to hit the net, raise an exception.

View File

@@ -1,6 +1,7 @@
import vcr
from urllib.request import urlopen from urllib.request import urlopen
import vcr
def true_matcher(r1, r2): def true_matcher(r1, r2):
return True return True
@@ -10,27 +11,27 @@ def false_matcher(r1, r2):
return False return False
def test_registered_true_matcher(tmpdir, httpbin): def test_registered_true_matcher(tmpdir, mockbin_request_url):
my_vcr = vcr.VCR() my_vcr = vcr.VCR()
my_vcr.register_matcher("true", true_matcher) my_vcr.register_matcher("true", true_matcher)
testfile = str(tmpdir.join("test.yml")) testfile = str(tmpdir.join("test.yml"))
with my_vcr.use_cassette(testfile, match_on=["true"]): with my_vcr.use_cassette(testfile, match_on=["true"]):
# These 2 different urls are stored as the same request # These 2 different urls are stored as the same request
urlopen(httpbin.url) urlopen(mockbin_request_url)
urlopen(httpbin.url + "/get") urlopen(mockbin_request_url + "/get")
with my_vcr.use_cassette(testfile, match_on=["true"]): with my_vcr.use_cassette(testfile, match_on=["true"]):
# I can get the response twice even though I only asked for it once # I can get the response twice even though I only asked for it once
urlopen(httpbin.url + "/get") urlopen(mockbin_request_url)
urlopen(httpbin.url + "/get") urlopen(mockbin_request_url)
def test_registered_false_matcher(tmpdir, httpbin): def test_registered_false_matcher(tmpdir, mockbin_request_url):
my_vcr = vcr.VCR() my_vcr = vcr.VCR()
my_vcr.register_matcher("false", false_matcher) my_vcr.register_matcher("false", false_matcher)
testfile = str(tmpdir.join("test.yml")) testfile = str(tmpdir.join("test.yml"))
with my_vcr.use_cassette(testfile, match_on=["false"]) as cass: with my_vcr.use_cassette(testfile, match_on=["false"]) as cass:
# These 2 different urls are stored as different requests # These 2 different urls are stored as different requests
urlopen(httpbin.url) urlopen(mockbin_request_url)
urlopen(httpbin.url + "/get") urlopen(mockbin_request_url + "/get")
assert len(cass) == 2 assert len(cass) == 2

View File

@@ -12,7 +12,7 @@ from vcr.persisters.filesystem import FilesystemPersister
class CustomFilesystemPersister(object): class CustomFilesystemPersister(object):
"""Behaves just like default FilesystemPersister but adds .test extension """Behaves just like default FilesystemPersister but adds .test extension
to the cassette file""" to the cassette file"""
@staticmethod @staticmethod
def load_cassette(cassette_path, serializer): def load_cassette(cassette_path, serializer):
@@ -30,7 +30,7 @@ def test_save_cassette_with_custom_persister(tmpdir, httpbin):
my_vcr = vcr.VCR() my_vcr = vcr.VCR()
my_vcr.register_persister(CustomFilesystemPersister) my_vcr.register_persister(CustomFilesystemPersister)
# Check to make sure directory doesnt exist # Check to make sure directory doesn't exist
assert not os.path.exists(str(tmpdir.join("nonexistent"))) assert not os.path.exists(str(tmpdir.join("nonexistent")))
# Run VCR to create dir and cassette file using new save_cassette callback # Run VCR to create dir and cassette file using new save_cassette callback

View File

@@ -1,6 +1,7 @@
import vcr
from urllib.request import urlopen from urllib.request import urlopen
import vcr
def test_recorded_request_uri_with_redirected_request(tmpdir, httpbin): def test_recorded_request_uri_with_redirected_request(tmpdir, httpbin):
with vcr.use_cassette(str(tmpdir.join("test.yml"))) as cass: with vcr.use_cassette(str(tmpdir.join("test.yml"))) as cass:

View File

@@ -1,11 +1,9 @@
# -*- coding: utf-8 -*-
"""Test requests' interaction with vcr""" """Test requests' interaction with vcr"""
import platform
import pytest import pytest
import sys
import vcr
from assertions import assert_cassette_empty, assert_is_json from assertions import assert_cassette_empty, assert_is_json
import vcr
requests = pytest.importorskip("requests") requests = pytest.importorskip("requests")
from requests.exceptions import ConnectionError # noqa E402 from requests.exceptions import ConnectionError # noqa E402
@@ -117,10 +115,6 @@ def test_post_chunked_binary(tmpdir, httpbin):
@pytest.mark.skipif("sys.version_info >= (3, 6)", strict=True, raises=ConnectionError) @pytest.mark.skipif("sys.version_info >= (3, 6)", strict=True, raises=ConnectionError)
@pytest.mark.skipif(
(3, 5) < sys.version_info < (3, 6) and platform.python_implementation() == "CPython",
reason="Fails on CPython 3.5",
)
def test_post_chunked_binary_secure(tmpdir, httpbin_secure): def test_post_chunked_binary_secure(tmpdir, httpbin_secure):
"""Ensure that we can send chunked binary without breaking while trying to concatenate bytes with str.""" """Ensure that we can send chunked binary without breaking while trying to concatenate bytes with str."""
data1 = iter([b"data", b"to", b"send"]) data1 = iter([b"data", b"to", b"send"])

View File

@@ -1,10 +1,11 @@
import vcr
import zlib
import json
import http.client as httplib import http.client as httplib
import json
import zlib
from assertions import assert_is_json from assertions import assert_is_json
import vcr
def _headers_are_case_insensitive(host, port): def _headers_are_case_insensitive(host, port):
conn = httplib.HTTPConnection(host, port) conn = httplib.HTTPConnection(host, port)
@@ -64,7 +65,7 @@ def test_original_decoded_response_is_not_modified(tmpdir, httpbin):
inside = conn.getresponse() inside = conn.getresponse()
# Assert that we do not modify the original response while appending # Assert that we do not modify the original response while appending
# to the casssette. # to the cassette.
assert "gzip" == inside.headers["content-encoding"] assert "gzip" == inside.headers["content-encoding"]
# They should effectively be the same response. # They should effectively be the same response.

View File

@@ -4,11 +4,11 @@
import json import json
import pytest import pytest
from assertions import assert_cassette_empty, assert_is_json
import vcr import vcr
from vcr.errors import CannotOverwriteExistingCassetteException from vcr.errors import CannotOverwriteExistingCassetteException
from assertions import assert_cassette_empty, assert_is_json
tornado = pytest.importorskip("tornado") tornado = pytest.importorskip("tornado")
http = pytest.importorskip("tornado.httpclient") http = pytest.importorskip("tornado.httpclient")
@@ -44,12 +44,6 @@ def post(client, url, data=None, **kwargs):
return client.fetch(http.HTTPRequest(url, method="POST", **kwargs)) return client.fetch(http.HTTPRequest(url, method="POST", **kwargs))
@pytest.fixture(params=["https", "http"])
def scheme(request):
"""Fixture that returns both http and https."""
return request.param
@pytest.mark.gen_test @pytest.mark.gen_test
def test_status_code(get_client, scheme, tmpdir): def test_status_code(get_client, scheme, tmpdir):
"""Ensure that we can read the status code""" """Ensure that we can read the status code"""
@@ -90,10 +84,10 @@ def test_body(get_client, tmpdir, scheme):
@pytest.mark.gen_test @pytest.mark.gen_test
def test_effective_url(get_client, scheme, tmpdir): def test_effective_url(get_client, scheme, tmpdir):
"""Ensure that the effective_url is captured""" """Ensure that the effective_url is captured"""
url = scheme + "://httpbin.org/redirect-to?url=/html" url = scheme + "://mockbin.org/redirect/301?url=/html"
with vcr.use_cassette(str(tmpdir.join("url.yaml"))): with vcr.use_cassette(str(tmpdir.join("url.yaml"))):
effective_url = (yield get(get_client(), url)).effective_url effective_url = (yield get(get_client(), url)).effective_url
assert effective_url == scheme + "://httpbin.org/html" assert effective_url == scheme + "://mockbin.org/redirect/301/0"
with vcr.use_cassette(str(tmpdir.join("url.yaml"))) as cass: with vcr.use_cassette(str(tmpdir.join("url.yaml"))) as cass:
assert effective_url == (yield get(get_client(), url)).effective_url assert effective_url == (yield get(get_client(), url)).effective_url
@@ -156,7 +150,7 @@ def test_post(get_client, tmpdir, scheme):
@pytest.mark.gen_test @pytest.mark.gen_test
def test_redirects(get_client, tmpdir, scheme): def test_redirects(get_client, tmpdir, scheme):
"""Ensure that we can handle redirects""" """Ensure that we can handle redirects"""
url = scheme + "://httpbin.org/redirect-to?url=bytes/1024" url = scheme + "://mockbin.org/redirect/301?url=bytes/1024"
with vcr.use_cassette(str(tmpdir.join("requests.yaml"))): with vcr.use_cassette(str(tmpdir.join("requests.yaml"))):
content = (yield get(get_client(), url)).body content = (yield get(get_client(), url)).body

View File

@@ -2,15 +2,15 @@
"""Integration tests with urllib2""" """Integration tests with urllib2"""
import ssl import ssl
from urllib.request import urlopen
from urllib.parse import urlencode from urllib.parse import urlencode
from urllib.request import urlopen
import pytest_httpbin.certs import pytest_httpbin.certs
from assertions import assert_cassette_has_one_response
# Internal imports # Internal imports
import vcr import vcr
from assertions import assert_cassette_has_one_response
def urlopen_with_cafile(*args, **kwargs): def urlopen_with_cafile(*args, **kwargs):
context = ssl.create_default_context(cafile=pytest_httpbin.certs.where()) context = ssl.create_default_context(cafile=pytest_httpbin.certs.where())
@@ -56,12 +56,13 @@ def test_response_headers(httpbin_both, tmpdir):
assert sorted(open1) == sorted(open2) assert sorted(open1) == sorted(open2)
def test_effective_url(httpbin_both, tmpdir): def test_effective_url(tmpdir):
"""Ensure that the effective_url is captured""" """Ensure that the effective_url is captured"""
url = httpbin_both.url + "/redirect-to?url=/html" url = "http://mockbin.org/redirect/301"
with vcr.use_cassette(str(tmpdir.join("headers.yaml"))): with vcr.use_cassette(str(tmpdir.join("headers.yaml"))):
effective_url = urlopen_with_cafile(url).geturl() effective_url = urlopen_with_cafile(url).geturl()
assert effective_url == httpbin_both.url + "/html" assert effective_url == "http://mockbin.org/redirect/301/0"
with vcr.use_cassette(str(tmpdir.join("headers.yaml"))): with vcr.use_cassette(str(tmpdir.join("headers.yaml"))):
assert effective_url == urlopen_with_cafile(url).geturl() assert effective_url == urlopen_with_cafile(url).geturl()

View File

@@ -4,9 +4,10 @@
import pytest import pytest
import pytest_httpbin import pytest_httpbin
from assertions import assert_cassette_empty, assert_is_json
import vcr import vcr
from vcr.patch import force_reset from vcr.patch import force_reset
from assertions import assert_cassette_empty, assert_is_json
urllib3 = pytest.importorskip("urllib3") urllib3 = pytest.importorskip("urllib3")
@@ -94,9 +95,10 @@ def test_post(tmpdir, httpbin_both, verify_pool_mgr):
assert req1 == req2 assert req1 == req2
def test_redirects(tmpdir, httpbin_both, verify_pool_mgr): def test_redirects(tmpdir, verify_pool_mgr):
"""Ensure that we can handle redirects""" """Ensure that we can handle redirects"""
url = httpbin_both.url + "/redirect-to?url=bytes/1024" url = "http://mockbin.org/redirect/301"
with vcr.use_cassette(str(tmpdir.join("verify_pool_mgr.yaml"))): with vcr.use_cassette(str(tmpdir.join("verify_pool_mgr.yaml"))):
content = verify_pool_mgr.request("GET", url).data content = verify_pool_mgr.request("GET", url).data
@@ -104,8 +106,9 @@ def test_redirects(tmpdir, httpbin_both, verify_pool_mgr):
assert content == verify_pool_mgr.request("GET", url).data assert content == verify_pool_mgr.request("GET", url).data
# Ensure that we've now cached *two* responses. One for the redirect # Ensure that we've now cached *two* responses. One for the redirect
# and one for the final fetch # and one for the final fetch
assert len(cass) == 2
assert cass.play_count == 2 assert len(cass) == 2
assert cass.play_count == 2
def test_cross_scheme(tmpdir, httpbin, httpbin_secure, verify_pool_mgr): def test_cross_scheme(tmpdir, httpbin, httpbin_secure, verify_pool_mgr):

View File

@@ -1,9 +1,10 @@
import http.client as httplib import http.client as httplib
import multiprocessing import multiprocessing
import pytest
from xmlrpc.client import ServerProxy from xmlrpc.client import ServerProxy
from xmlrpc.server import SimpleXMLRPCServer from xmlrpc.server import SimpleXMLRPCServer
import pytest
requests = pytest.importorskip("requests") requests = pytest.importorskip("requests")
import vcr # NOQA import vcr # NOQA
@@ -83,7 +84,7 @@ def start_rpc_server(q):
httpd.serve_forever() httpd.serve_forever()
@pytest.yield_fixture(scope="session") @pytest.fixture(scope="session")
def rpc_server(): def rpc_server():
q = multiprocessing.Queue() q = multiprocessing.Queue()
proxy_process = multiprocessing.Process(target=start_rpc_server, args=(q,)) proxy_process = multiprocessing.Process(target=start_rpc_server, args=(q,))

View File

@@ -1,10 +1,10 @@
import contextlib import contextlib
import copy import copy
import inspect
import mock
import os
import http.client as httplib import http.client as httplib
import inspect
import os
from unittest import mock
import pytest import pytest
import yaml import yaml
@@ -138,6 +138,31 @@ def test_cassette_all_played():
assert a.all_played assert a.all_played
@mock.patch("vcr.cassette.requests_match", _mock_requests_match)
def test_cassette_allow_playback_repeats():
a = Cassette("test", allow_playback_repeats=True)
a.append("foo", "bar")
a.append("other", "resp")
for x in range(10):
assert a.play_response("foo") == "bar"
assert a.play_count == 10
assert a.all_played is False
assert a.play_response("other") == "resp"
assert a.play_count == 11
assert a.all_played
a.allow_playback_repeats = False
with pytest.raises(UnhandledHTTPRequestError) as e:
a.play_response("foo")
assert str(e.value) == "\"The cassette ('test') doesn't contain the request ('foo') asked for\""
a.rewind()
assert a.all_played is False
assert a.play_response("foo") == "bar"
assert a.all_played is False
assert a.play_response("other") == "resp"
assert a.all_played
@mock.patch("vcr.cassette.requests_match", _mock_requests_match) @mock.patch("vcr.cassette.requests_match", _mock_requests_match)
def test_cassette_rewound(): def test_cassette_rewound():
a = Cassette("test") a = Cassette("test")
@@ -184,7 +209,7 @@ def test_nesting_cassette_context_managers(*args):
) )
assert_get_response_body_is("first_response") assert_get_response_body_is("first_response")
# Make sure a second cassette can supercede the first # Make sure a second cassette can supersede the first
with Cassette.use(path="test") as second_cassette: with Cassette.use(path="test") as second_cassette:
with mock.patch.object(second_cassette, "play_response", return_value=second_response): with mock.patch.object(second_cassette, "play_response", return_value=second_response):
assert_get_response_body_is("second_response") assert_get_response_body_is("second_response")
@@ -286,16 +311,16 @@ def test_func_path_generator():
def test_use_as_decorator_on_coroutine(): def test_use_as_decorator_on_coroutine():
original_http_connetion = httplib.HTTPConnection original_http_connection = httplib.HTTPConnection
@Cassette.use(inject=True) @Cassette.use(inject=True)
def test_function(cassette): def test_function(cassette):
assert httplib.HTTPConnection.cassette is cassette assert httplib.HTTPConnection.cassette is cassette
assert httplib.HTTPConnection is not original_http_connetion assert httplib.HTTPConnection is not original_http_connection
value = yield 1 value = yield 1
assert value == 1 assert value == 1
assert httplib.HTTPConnection.cassette is cassette assert httplib.HTTPConnection.cassette is cassette
assert httplib.HTTPConnection is not original_http_connetion assert httplib.HTTPConnection is not original_http_connection
value = yield 2 value = yield 2
assert value == 2 assert value == 2
@@ -309,15 +334,15 @@ def test_use_as_decorator_on_coroutine():
def test_use_as_decorator_on_generator(): def test_use_as_decorator_on_generator():
original_http_connetion = httplib.HTTPConnection original_http_connection = httplib.HTTPConnection
@Cassette.use(inject=True) @Cassette.use(inject=True)
def test_function(cassette): def test_function(cassette):
assert httplib.HTTPConnection.cassette is cassette assert httplib.HTTPConnection.cassette is cassette
assert httplib.HTTPConnection is not original_http_connetion assert httplib.HTTPConnection is not original_http_connection
yield 1 yield 1
assert httplib.HTTPConnection.cassette is cassette assert httplib.HTTPConnection.cassette is cassette
assert httplib.HTTPConnection is not original_http_connetion assert httplib.HTTPConnection is not original_http_connection
yield 2 yield 2
assert list(test_function()) == [1, 2] assert list(test_function()) == [1, 2]

View File

@@ -1,4 +1,4 @@
import mock from unittest import mock
import pytest import pytest

View File

@@ -1,18 +1,19 @@
from io import BytesIO
from vcr.filters import (
remove_headers,
replace_headers,
remove_query_parameters,
replace_query_parameters,
remove_post_data_parameters,
replace_post_data_parameters,
decode_response,
)
from vcr.request import Request
import gzip import gzip
import json import json
import mock
import zlib import zlib
from io import BytesIO
from unittest import mock
from vcr.filters import (
decode_response,
remove_headers,
remove_post_data_parameters,
remove_query_parameters,
replace_headers,
replace_post_data_parameters,
replace_query_parameters,
)
from vcr.request import Request
def test_replace_headers(): def test_replace_headers():
@@ -220,6 +221,49 @@ def test_remove_all_json_post_data_parameters():
assert request.body == b"{}" assert request.body == b"{}"
def test_replace_dict_post_data_parameters():
# This tests all of:
# 1. keeping a parameter
# 2. removing a parameter
# 3. replacing a parameter
# 4. replacing a parameter using a callable
# 5. removing a parameter using a callable
# 6. replacing a parameter that doesn't exist
body = {"one": "keep", "two": "lose", "three": "change", "four": "shout", "five": "whisper"}
request = Request("POST", "http://google.com", body, {})
request.headers["Content-Type"] = "application/x-www-form-urlencoded"
replace_post_data_parameters(
request,
[
("two", None),
("three", "tada"),
("four", lambda key, value, request: value.upper()),
("five", lambda key, value, request: None),
("six", "doesntexist"),
],
)
expected_data = {"one": "keep", "three": "tada", "four": "SHOUT"}
assert request.body == expected_data
def test_remove_dict_post_data_parameters():
# Test the backward-compatible API wrapper.
body = {"id": "secret", "foo": "bar", "baz": "qux"}
request = Request("POST", "http://google.com", body, {})
request.headers["Content-Type"] = "application/x-www-form-urlencoded"
remove_post_data_parameters(request, ["id"])
expected_data = {"foo": "bar", "baz": "qux"}
assert request.body == expected_data
def test_remove_all_dict_post_data_parameters():
body = {"id": "secret", "foo": "bar"}
request = Request("POST", "http://google.com", body, {})
request.headers["Content-Type"] = "application/x-www-form-urlencoded"
replace_post_data_parameters(request, [("id", None), ("foo", None)])
assert request.body == {}
def test_decode_response_uncompressed(): def test_decode_response_uncompressed():
recorded_response = { recorded_response = {
"status": {"message": "OK", "code": 200}, "status": {"message": "OK", "code": 200},

View File

@@ -1,6 +1,7 @@
import pytest import pytest
from vcr.serializers.jsonserializer import serialize
from vcr.request import Request from vcr.request import Request
from vcr.serializers.jsonserializer import serialize
def test_serialize_binary(): def test_serialize_binary():

View File

@@ -1,10 +1,9 @@
import itertools import itertools
import mock from unittest import mock
import pytest import pytest
from vcr import matchers from vcr import matchers, request
from vcr import request
# the dict contains requests with corresponding to its key difference # the dict contains requests with corresponding to its key difference
# with 'base' request. # with 'base' request.

View File

@@ -1,6 +1,7 @@
import filecmp import filecmp
import json import json
import shutil import shutil
import yaml import yaml
import vcr.migration import vcr.migration
@@ -44,4 +45,4 @@ def test_try_migrate_with_invalid_or_new_cassettes(tmpdir):
for file_path in files: for file_path in files:
shutil.copy(file_path, cassette) shutil.copy(file_path, cassette)
assert not vcr.migration.try_migrate(cassette) assert not vcr.migration.try_migrate(cassette)
assert filecmp.cmp(cassette, file_path) # shold not change file assert filecmp.cmp(cassette, file_path) # should not change file

View File

@@ -1,6 +1,6 @@
import pytest import pytest
from vcr.request import Request, HeadersDict from vcr.request import HeadersDict, Request
@pytest.mark.parametrize( @pytest.mark.parametrize(
@@ -60,7 +60,6 @@ def test_uri(method, uri):
def test_HeadersDict(): def test_HeadersDict():
# Simple test of CaseInsensitiveDict # Simple test of CaseInsensitiveDict
h = HeadersDict() h = HeadersDict()
assert h == {} assert h == {}

View File

@@ -1,11 +1,11 @@
# -*- encoding: utf-8 -*- # -*- encoding: utf-8 -*-
import mock from unittest import mock
import pytest import pytest
from vcr.request import Request from vcr.request import Request
from vcr.serialize import deserialize, serialize from vcr.serialize import deserialize, serialize
from vcr.serializers import yamlserializer, jsonserializer, compat from vcr.serializers import compat, jsonserializer, yamlserializer
def test_deserialize_old_yaml_cassette(): def test_deserialize_old_yaml_cassette():

View File

@@ -1,11 +1,12 @@
import mock from unittest import mock
from vcr.stubs import VCRHTTPSConnection from vcr import mode
from vcr.cassette import Cassette from vcr.cassette import Cassette
from vcr.stubs import VCRHTTPSConnection
class TestVCRConnection: class TestVCRConnection:
def test_setting_of_attributes_get_propogated_to_real_connection(self): def test_setting_of_attributes_get_propagated_to_real_connection(self):
vcr_connection = VCRHTTPSConnection("www.examplehost.com") vcr_connection = VCRHTTPSConnection("www.examplehost.com")
vcr_connection.ssl_version = "example_ssl_version" vcr_connection.ssl_version = "example_ssl_version"
assert vcr_connection.real_connection.ssl_version == "example_ssl_version" assert vcr_connection.real_connection.ssl_version == "example_ssl_version"
@@ -13,6 +14,6 @@ class TestVCRConnection:
@mock.patch("vcr.cassette.Cassette.can_play_response_for", return_value=False) @mock.patch("vcr.cassette.Cassette.can_play_response_for", return_value=False)
def testing_connect(*args): def testing_connect(*args):
vcr_connection = VCRHTTPSConnection("www.google.com") vcr_connection = VCRHTTPSConnection("www.google.com")
vcr_connection.cassette = Cassette("test", record_mode="all") vcr_connection.cassette = Cassette("test", record_mode=mode.ALL)
vcr_connection.real_connection.connect() vcr_connection.real_connection.connect()
assert vcr_connection.real_connection.sock is not None assert vcr_connection.real_connection.sock is not None

View File

@@ -1,13 +1,14 @@
import mock import http.client as httplib
import os import os
from pathlib import Path
from unittest import mock
import pytest import pytest
import http.client as httplib
from vcr import VCR, use_cassette from vcr import VCR, mode, use_cassette
from vcr.patch import _HTTPConnection, force_reset
from vcr.request import Request from vcr.request import Request
from vcr.stubs import VCRHTTPSConnection from vcr.stubs import VCRHTTPSConnection
from vcr.patch import _HTTPConnection, force_reset
def test_vcr_use_cassette(): def test_vcr_use_cassette():
@@ -31,7 +32,7 @@ def test_vcr_use_cassette():
function() function()
assert mock_cassette_load.call_args[1]["record_mode"] == test_vcr.record_mode assert mock_cassette_load.call_args[1]["record_mode"] == test_vcr.record_mode
# Ensure that explicitly provided arguments still supercede # Ensure that explicitly provided arguments still supersede
# those on the vcr. # those on the vcr.
new_record_mode = mock.Mock() new_record_mode = mock.Mock()
@@ -95,7 +96,6 @@ def test_vcr_before_record_response_iterable():
# Prevent actually saving the cassette # Prevent actually saving the cassette
with mock.patch("vcr.cassette.FilesystemPersister.save_cassette"): with mock.patch("vcr.cassette.FilesystemPersister.save_cassette"):
# Baseline: non-iterable before_record_response should work # Baseline: non-iterable before_record_response should work
mock_filter = mock.Mock() mock_filter = mock.Mock()
vcr = VCR(before_record_response=mock_filter) vcr = VCR(before_record_response=mock_filter)
@@ -119,7 +119,6 @@ def test_before_record_response_as_filter():
# Prevent actually saving the cassette # Prevent actually saving the cassette
with mock.patch("vcr.cassette.FilesystemPersister.save_cassette"): with mock.patch("vcr.cassette.FilesystemPersister.save_cassette"):
filter_all = mock.Mock(return_value=None) filter_all = mock.Mock(return_value=None)
vcr = VCR(before_record_response=filter_all) vcr = VCR(before_record_response=filter_all)
with vcr.use_cassette("test") as cassette: with vcr.use_cassette("test") as cassette:
@@ -133,7 +132,6 @@ def test_vcr_path_transformer():
# Prevent actually saving the cassette # Prevent actually saving the cassette
with mock.patch("vcr.cassette.FilesystemPersister.save_cassette"): with mock.patch("vcr.cassette.FilesystemPersister.save_cassette"):
# Baseline: path should be unchanged # Baseline: path should be unchanged
vcr = VCR() vcr = VCR()
with vcr.use_cassette("test") as cassette: with vcr.use_cassette("test") as cassette:
@@ -188,11 +186,11 @@ def test_custom_patchers():
def test_inject_cassette(): def test_inject_cassette():
vcr = VCR(inject_cassette=True) vcr = VCR(inject_cassette=True)
@vcr.use_cassette("test", record_mode="once") @vcr.use_cassette("test", record_mode=mode.ONCE)
def with_cassette_injected(cassette): def with_cassette_injected(cassette):
assert cassette.record_mode == "once" assert cassette.record_mode == mode.ONCE
@vcr.use_cassette("test", record_mode="once", inject_cassette=False) @vcr.use_cassette("test", record_mode=mode.ONCE, inject_cassette=False)
def without_cassette_injected(): def without_cassette_injected():
pass pass
@@ -201,7 +199,7 @@ def test_inject_cassette():
def test_with_current_defaults(): def test_with_current_defaults():
vcr = VCR(inject_cassette=True, record_mode="once") vcr = VCR(inject_cassette=True, record_mode=mode.ONCE)
@vcr.use_cassette("test", with_current_defaults=False) @vcr.use_cassette("test", with_current_defaults=False)
def changing_defaults(cassette, checks): def changing_defaults(cassette, checks):
@@ -212,10 +210,10 @@ def test_with_current_defaults():
checks(cassette) checks(cassette)
def assert_record_mode_once(cassette): def assert_record_mode_once(cassette):
assert cassette.record_mode == "once" assert cassette.record_mode == mode.ONCE
def assert_record_mode_all(cassette): def assert_record_mode_all(cassette):
assert cassette.record_mode == "all" assert cassette.record_mode == mode.ALL
changing_defaults(assert_record_mode_once) changing_defaults(assert_record_mode_once)
current_defaults(assert_record_mode_once) current_defaults(assert_record_mode_once)
@@ -226,7 +224,7 @@ def test_with_current_defaults():
def test_cassette_library_dir_with_decoration_and_no_explicit_path(): def test_cassette_library_dir_with_decoration_and_no_explicit_path():
library_dir = "/libary_dir" library_dir = "/library_dir"
vcr = VCR(inject_cassette=True, cassette_library_dir=library_dir) vcr = VCR(inject_cassette=True, cassette_library_dir=library_dir)
@vcr.use_cassette() @vcr.use_cassette()
@@ -237,7 +235,7 @@ def test_cassette_library_dir_with_decoration_and_no_explicit_path():
def test_cassette_library_dir_with_decoration_and_explicit_path(): def test_cassette_library_dir_with_decoration_and_explicit_path():
library_dir = "/libary_dir" library_dir = "/library_dir"
vcr = VCR(inject_cassette=True, cassette_library_dir=library_dir) vcr = VCR(inject_cassette=True, cassette_library_dir=library_dir)
@vcr.use_cassette(path="custom_name") @vcr.use_cassette(path="custom_name")
@@ -248,7 +246,7 @@ def test_cassette_library_dir_with_decoration_and_explicit_path():
def test_cassette_library_dir_with_decoration_and_super_explicit_path(): def test_cassette_library_dir_with_decoration_and_super_explicit_path():
library_dir = "/libary_dir" library_dir = "/library_dir"
vcr = VCR(inject_cassette=True, cassette_library_dir=library_dir) vcr = VCR(inject_cassette=True, cassette_library_dir=library_dir)
@vcr.use_cassette(path=os.path.join(library_dir, "custom_name")) @vcr.use_cassette(path=os.path.join(library_dir, "custom_name"))
@@ -259,7 +257,7 @@ def test_cassette_library_dir_with_decoration_and_super_explicit_path():
def test_cassette_library_dir_with_path_transformer(): def test_cassette_library_dir_with_path_transformer():
library_dir = "/libary_dir" library_dir = "/library_dir"
vcr = VCR( vcr = VCR(
inject_cassette=True, cassette_library_dir=library_dir, path_transformer=lambda path: path + ".json" inject_cassette=True, cassette_library_dir=library_dir, path_transformer=lambda path: path + ".json"
) )
@@ -360,3 +358,11 @@ def test_dynamically_added(self):
TestVCRClass.test_dynamically_added = test_dynamically_added TestVCRClass.test_dynamically_added = test_dynamically_added
del test_dynamically_added 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"
)
with use_cassette(path):
pass

View File

@@ -2,7 +2,6 @@ import sys
def test_vcr_import_deprecation(recwarn): def test_vcr_import_deprecation(recwarn):
if "vcr" in sys.modules: if "vcr" in sys.modules:
# Remove imported module entry if already loaded in another test # Remove imported module entry if already loaded in another test
del sys.modules["vcr"] del sys.modules["vcr"]

52
tox.ini
View File

@@ -1,13 +1,23 @@
[tox] [tox]
skip_missing_interpreters=true skip_missing_interpreters=true
envlist = envlist =
cov-clean, cov-clean,
lint, lint,
{py35,py36,py37,py38}-{requests,httplib2,urllib3,tornado4,boto3,aiohttp}, {py37,py38,py39,py310,py311}-{requests,httplib2,urllib3,tornado4,boto3,aiohttp,httpx},
{pypy3}-{requests,httplib2,urllib3,tornado4,boto3}, {pypy3}-{requests,httplib2,urllib3,tornado4,boto3},
{py310}-httpx019,
cov-report cov-report
[gh-actions]
python =
3.7: py37
3.8: py38
3.9: py39
3.10: py310, lint
3.11: py311
pypy-3: pypy3
# Coverage environment tasks: cov-clean and cov-report # Coverage environment tasks: cov-clean and cov-report
# https://pytest-cov.readthedocs.io/en/latest/tox.html # https://pytest-cov.readthedocs.io/en/latest/tox.html
[testenv:cov-clean] [testenv:cov-clean]
@@ -18,7 +28,7 @@ commands = coverage erase
[testenv:cov-report] [testenv:cov-report]
deps = coverage deps = coverage
skip_install=true skip_install=true
commands = commands =
coverage html coverage html
coverage report --fail-under=90 coverage report --fail-under=90
@@ -27,12 +37,16 @@ skipsdist = True
commands = commands =
black --version black --version
black --check --diff . black --check --diff .
isort --version
isort . --check --diff
flake8 --version flake8 --version
flake8 --exclude=./docs/conf.py,./.tox/ flake8 --exclude=./docs/conf.py,./.tox/,./venv/
pyflakes ./docs/conf.py pyflakes ./docs/conf.py
deps = deps =
flake8 flake8
black black
isort
basepython = python3.10
[testenv:docs] [testenv:docs]
# Running sphinx from inside the "docs" directory # Running sphinx from inside the "docs" directory
@@ -61,28 +75,34 @@ basepython = python3.7
usedevelop=true usedevelop=true
commands = commands =
./runtests.sh --cov=./vcr --cov-branch --cov-report=xml --cov-append {posargs} ./runtests.sh --cov=./vcr --cov-branch --cov-report=xml --cov-append {posargs}
allowlist_externals =
./runtests.sh
deps = deps =
Flask Werkzeug==2.0.3
mock
pytest pytest
pytest-httpbin pytest-httpbin>=1.0.1
pytest-cov pytest-cov
PyYAML PyYAML
ipaddress ipaddress
requests: requests>=2.22.0 requests: requests>=2.22.0
requests: urllib3<2
httplib2: httplib2 httplib2: httplib2
urllib3: urllib3 urllib3: urllib3<2
{py35,py36}-tornado4: tornado>=4,<5
{py35,py36}-tornado4: pytest-tornado
{py35,py36}-tornado4: pycurl
boto3: boto3 boto3: boto3
boto3: urllib3 boto3: urllib3
aiohttp: aiohttp aiohttp: aiohttp
aiohttp: pytest-asyncio aiohttp: pytest-asyncio
aiohttp: pytest-aiohttp aiohttp: pytest-aiohttp
depends = httpx: httpx
lint,{py35,py36,py37,py38,pypy3}-{requests,httplib2,urllib3,tornado4,boto3},{py35,py36,py37,py38}-{aiohttp}: cov-clean {py37,py38,py39,py310}-{httpx}: httpx
cov-report: lint,{py35,py36,py37,py38,pypy3}-{requests,httplib2,urllib3,tornado4,boto3},{py35,py36,py37,py38}-{aiohttp} {py37,py38,py39,py310}-{httpx}: pytest-asyncio
httpx: httpx>0.19
# httpx==0.19 is the latest version that supports allow_redirects, newer versions use follow_redirects
httpx019: httpx==0.19
{py37,py38,py39,py310}-{httpx}: pytest-asyncio
depends =
lint,{py37,py38,py39,py310,py311,pypy3}-{requests,httplib2,urllib3,tornado4,boto3},{py37,py38,py39,py310,py311}-{aiohttp},{py37,py38,py39,py310,py311}-{httpx}: cov-clean
cov-report: lint,{py37,py38,py39,py310,py311,pypy3}-{requests,httplib2,urllib3,tornado4,boto3},{py37,py38,py39,py310,py311}-{aiohttp}
passenv = passenv =
AWS_ACCESS_KEY_ID AWS_ACCESS_KEY_ID
AWS_DEFAULT_REGION AWS_DEFAULT_REGION

View File

@@ -1,8 +1,10 @@
import logging import logging
from .config import VCR
from logging import NullHandler from logging import NullHandler
__version__ = "4.0.1" from .config import VCR
from .record_mode import RecordMode as mode # noqa import is not used in this file
__version__ = "4.3.0"
logging.getLogger(__name__).addHandler(NullHandler()) logging.getLogger(__name__).addHandler(NullHandler())

View File

@@ -1,19 +1,20 @@
import collections import collections
import contextlib import contextlib
import copy import copy
import sys
import inspect import inspect
import logging import logging
import sys
import wrapt import wrapt
from .errors import UnhandledHTTPRequestError
from .matchers import requests_match, uri, method, get_matchers_results
from .patch import CassettePatcherBuilder
from .serializers import yamlserializer
from .persisters.filesystem import FilesystemPersister
from .util import partition_dict
from ._handle_coroutine import handle_coroutine from ._handle_coroutine import handle_coroutine
from .errors import UnhandledHTTPRequestError
from .matchers import get_matchers_results, method, requests_match, uri
from .patch import CassettePatcherBuilder
from .persisters.filesystem import FilesystemPersister
from .record_mode import RecordMode
from .serializers import yamlserializer
from .util import partition_dict
try: try:
from asyncio import iscoroutinefunction from asyncio import iscoroutinefunction
@@ -44,7 +45,11 @@ class CassetteContextDecorator:
this class as a context manager in ``__exit__``. this class as a context manager in ``__exit__``.
""" """
_non_cassette_arguments = ("path_transformer", "func_path_generator") _non_cassette_arguments = (
"path_transformer",
"func_path_generator",
"record_on_exception",
)
@classmethod @classmethod
def from_args(cls, cassette_class, **kwargs): def from_args(cls, cassette_class, **kwargs):
@@ -54,6 +59,7 @@ class CassetteContextDecorator:
self.cls = cls self.cls = cls
self._args_getter = args_getter self._args_getter = args_getter
self.__finish = None self.__finish = None
self.__cassette = None
def _patch_generator(self, cassette): def _patch_generator(self, cassette):
with contextlib.ExitStack() as exit_stack: with contextlib.ExitStack() as exit_stack:
@@ -63,9 +69,6 @@ class CassetteContextDecorator:
log.debug(log_format.format(action="Entering", path=cassette._path)) log.debug(log_format.format(action="Entering", path=cassette._path))
yield cassette yield cassette
log.debug(log_format.format(action="Exiting", path=cassette._path)) log.debug(log_format.format(action="Exiting", path=cassette._path))
# TODO(@IvanMalison): Hmmm. it kind of feels like this should be
# somewhere else.
cassette._save()
def __enter__(self): def __enter__(self):
# This assertion is here to prevent the dangerous behavior # This assertion is here to prevent the dangerous behavior
@@ -83,10 +86,22 @@ class CassetteContextDecorator:
if other_kwargs.get("path_transformer"): if other_kwargs.get("path_transformer"):
transformer = other_kwargs["path_transformer"] transformer = other_kwargs["path_transformer"]
cassette_kwargs["path"] = transformer(cassette_kwargs["path"]) cassette_kwargs["path"] = transformer(cassette_kwargs["path"])
self.__finish = self._patch_generator(self.cls.load(**cassette_kwargs)) self.__cassette = self.cls.load(**cassette_kwargs)
self.__finish = self._patch_generator(self.__cassette)
return next(self.__finish) return next(self.__finish)
def __exit__(self, *args): def __exit__(self, *exc_info):
exception_was_raised = any(exc_info)
record_on_exception = self._args_getter().get("record_on_exception", True)
if record_on_exception or not exception_was_raised:
self.__cassette._save()
self.__cassette = None
# Fellow programmer, don't remove this `next`, if `self.__finish` is
# not consumed the unpatcher functions accumulated in the `exit_stack`
# object created in `_patch_generator` will not be called until
# `exit_stack` is not garbage collected.
# This works in CPython but not in Pypy, where the unpatchers will not
# be called until much later.
next(self.__finish, None) next(self.__finish, None)
self.__finish = None self.__finish = None
@@ -175,12 +190,13 @@ class Cassette:
path, path,
serializer=None, serializer=None,
persister=None, persister=None,
record_mode="once", record_mode=RecordMode.ONCE,
match_on=(uri, method), match_on=(uri, method),
before_record_request=None, before_record_request=None,
before_record_response=None, before_record_response=None,
custom_patches=(), custom_patches=(),
inject=False, inject=False,
allow_playback_repeats=False,
): ):
self._persister = persister or FilesystemPersister self._persister = persister or FilesystemPersister
self._path = path self._path = path
@@ -192,6 +208,7 @@ class Cassette:
self.inject = inject self.inject = inject
self.record_mode = record_mode self.record_mode = record_mode
self.custom_patches = custom_patches self.custom_patches = custom_patches
self.allow_playback_repeats = allow_playback_repeats
# self.data is the list of (req, resp) tuples # self.data is the list of (req, resp) tuples
self.data = [] self.data = []
@@ -206,7 +223,7 @@ class Cassette:
@property @property
def all_played(self): def all_played(self):
"""Returns True if all responses have been played, False otherwise.""" """Returns True if all responses have been played, False otherwise."""
return self.play_count == len(self) return len(self.play_counts.values()) == len(self)
@property @property
def requests(self): def requests(self):
@@ -218,7 +235,7 @@ class Cassette:
@property @property
def write_protected(self): def write_protected(self):
return self.rewound and self.record_mode == "once" or self.record_mode == "none" return self.rewound and self.record_mode == RecordMode.ONCE or self.record_mode == RecordMode.NONE
def append(self, request, response): def append(self, request, response):
"""Add a request, response pair to this cassette""" """Add a request, response pair to this cassette"""
@@ -250,7 +267,7 @@ class Cassette:
def can_play_response_for(self, request): def can_play_response_for(self, request):
request = self._before_record_request(request) request = self._before_record_request(request)
return request and request in self and self.record_mode != "all" and self.rewound return request and request in self and self.record_mode != RecordMode.ALL and self.rewound
def play_response(self, request): def play_response(self, request):
""" """
@@ -258,7 +275,7 @@ class Cassette:
hasn't been played back before, and mark it as played hasn't been played back before, and mark it as played
""" """
for index, response in self._responses(request): for index, response in self._responses(request):
if self.play_counts[index] == 0: if self.play_counts[index] == 0 or self.allow_playback_repeats:
self.play_counts[index] += 1 self.play_counts[index] += 1
return response return response
# The cassette doesn't contain the request asked for. # The cassette doesn't contain the request asked for.
@@ -348,6 +365,6 @@ class Cassette:
def __contains__(self, request): def __contains__(self, request):
"""Return whether or not a request has been stored""" """Return whether or not a request has been stored"""
for index, response in self._responses(request): for index, response in self._responses(request):
if self.play_counts[index] == 0: if self.play_counts[index] == 0 or self.allow_playback_repeats:
return True return True
return False return False

View File

@@ -1,19 +1,19 @@
import copy import copy
from collections import abc as collections_abc
import functools import functools
import inspect import inspect
import os import os
import types import types
from collections import abc as collections_abc
from pathlib import Path
import six import six
from . import filters, matchers
from .cassette import Cassette from .cassette import Cassette
from .serializers import yamlserializer, jsonserializer
from .persisters.filesystem import FilesystemPersister from .persisters.filesystem import FilesystemPersister
from .util import compose, auto_decorate from .record_mode import RecordMode
from . import matchers from .serializers import jsonserializer, yamlserializer
from . import filters from .util import auto_decorate, compose
class VCR: class VCR:
@@ -37,7 +37,7 @@ class VCR:
custom_patches=(), custom_patches=(),
filter_query_parameters=(), filter_query_parameters=(),
ignore_hosts=(), ignore_hosts=(),
record_mode="once", record_mode=RecordMode.ONCE,
ignore_localhost=False, ignore_localhost=False,
filter_headers=(), filter_headers=(),
before_record_response=None, before_record_response=None,
@@ -49,6 +49,7 @@ class VCR:
cassette_library_dir=None, cassette_library_dir=None,
func_path_generator=None, func_path_generator=None,
decode_compressed_response=False, decode_compressed_response=False,
record_on_exception=True,
): ):
self.serializer = serializer self.serializer = serializer
self.match_on = match_on self.match_on = match_on
@@ -80,6 +81,7 @@ class VCR:
self.path_transformer = path_transformer self.path_transformer = path_transformer
self.func_path_generator = func_path_generator self.func_path_generator = func_path_generator
self.decode_compressed_response = decode_compressed_response self.decode_compressed_response = decode_compressed_response
self.record_on_exception = record_on_exception
self._custom_patches = tuple(custom_patches) self._custom_patches = tuple(custom_patches)
def _get_serializer(self, serializer_name): def _get_serializer(self, serializer_name):
@@ -99,7 +101,7 @@ class VCR:
return matchers return matchers
def use_cassette(self, path=None, **kwargs): def use_cassette(self, path=None, **kwargs):
if path is not None and not isinstance(path, str): if path is not None and not isinstance(path, (str, Path)):
function = path function = path
# Assume this is an attempt to decorate a function # Assume this is an attempt to decorate a function
return self._use_cassette(**kwargs)(function) return self._use_cassette(**kwargs)(function)
@@ -123,6 +125,7 @@ class VCR:
func_path_generator = kwargs.get("func_path_generator", self.func_path_generator) func_path_generator = kwargs.get("func_path_generator", self.func_path_generator)
cassette_library_dir = kwargs.get("cassette_library_dir", self.cassette_library_dir) cassette_library_dir = kwargs.get("cassette_library_dir", self.cassette_library_dir)
additional_matchers = kwargs.get("additional_matchers", ()) additional_matchers = kwargs.get("additional_matchers", ())
record_on_exception = kwargs.get("record_on_exception", self.record_on_exception)
if cassette_library_dir: if cassette_library_dir:
@@ -148,6 +151,8 @@ class VCR:
"inject": kwargs.get("inject_cassette", self.inject_cassette), "inject": kwargs.get("inject_cassette", self.inject_cassette),
"path_transformer": path_transformer, "path_transformer": path_transformer,
"func_path_generator": func_path_generator, "func_path_generator": func_path_generator,
"allow_playback_repeats": kwargs.get("allow_playback_repeats", False),
"record_on_exception": record_on_exception,
} }
path = kwargs.get("path") path = kwargs.get("path")
if path: if path:

View File

@@ -1,8 +1,8 @@
from io import BytesIO
from urllib.parse import urlparse, urlencode, urlunparse
import copy import copy
import json import json
import zlib import zlib
from io import BytesIO
from urllib.parse import urlencode, urlparse, urlunparse
from .util import CaseInsensitiveDict from .util import CaseInsensitiveDict
@@ -84,7 +84,17 @@ def replace_post_data_parameters(request, replacements):
replacements = dict(replacements) replacements = dict(replacements)
if request.method == "POST" and not isinstance(request.body, BytesIO): if request.method == "POST" and not isinstance(request.body, BytesIO):
if request.headers.get("Content-Type") == "application/json": if isinstance(request.body, dict):
new_body = request.body.copy()
for k, rv in replacements.items():
if k in new_body:
ov = new_body.pop(k)
if callable(rv):
rv = rv(key=k, value=ov, request=request)
if rv is not None:
new_body[k] = rv
request.body = new_body
elif request.headers.get("Content-Type") == "application/json":
json_data = json.loads(request.body.decode("utf-8")) json_data = json.loads(request.body.decode("utf-8"))
for k, rv in replacements.items(): for k, rv in replacements.items():
if k in json_data: if k in json_data:
@@ -140,6 +150,8 @@ def decode_response(response):
"""Returns decompressed body according to encoding using zlib. """Returns decompressed body according to encoding using zlib.
to (de-)compress gzip format, use wbits = zlib.MAX_WBITS | 16 to (de-)compress gzip format, use wbits = zlib.MAX_WBITS | 16
""" """
if not body:
return ""
if encoding == "gzip": if encoding == "gzip":
return zlib.decompress(body, zlib.MAX_WBITS | 16) return zlib.decompress(body, zlib.MAX_WBITS | 16)
else: # encoding == 'deflate' else: # encoding == 'deflate'

View File

@@ -1,9 +1,9 @@
import json import json
import logging
import urllib import urllib
import xmlrpc.client import xmlrpc.client
from .util import read_body
import logging
from .util import read_body
log = logging.getLogger(__name__) log = logging.getLogger(__name__)

View File

@@ -17,11 +17,12 @@ import os
import shutil import shutil
import sys import sys
import tempfile import tempfile
import yaml import yaml
from .serializers import yamlserializer, jsonserializer
from .serialize import serialize
from . import request from . import request
from .serialize import serialize
from .serializers import jsonserializer, yamlserializer
from .stubs.compat import get_httpmessage from .stubs.compat import get_httpmessage
# Use the libYAML versions if possible # Use the libYAML versions if possible

View File

@@ -1,13 +1,12 @@
"""Utilities for patching in cassettes""" """Utilities for patching in cassettes"""
import contextlib import contextlib
import functools import functools
import http.client as httplib
import itertools import itertools
import mock import logging
from unittest import mock
from .stubs import VCRHTTPConnection, VCRHTTPSConnection from .stubs import VCRHTTPConnection, VCRHTTPSConnection
import http.client as httplib
import logging
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
# Save some of the original types for the purposes of unpatching # Save some of the original types for the purposes of unpatching
@@ -16,16 +15,17 @@ _HTTPSConnection = httplib.HTTPSConnection
# Try to save the original types for boto3 # Try to save the original types for boto3
try: try:
from botocore.awsrequest import AWSHTTPSConnection, AWSHTTPConnection from botocore.awsrequest import AWSHTTPConnection, AWSHTTPSConnection
except ImportError: except ImportError as e:
try: try:
import botocore.vendored.requests.packages.urllib3.connectionpool as cpool import botocore.vendored.requests # noqa: F401
except ImportError: # pragma: no cover except ImportError: # pragma: no cover
pass pass
else: else:
_Boto3VerifiedHTTPSConnection = cpool.VerifiedHTTPSConnection raise RuntimeError(
_cpoolBoto3HTTPConnection = cpool.HTTPConnection "vcrpy >=4.2.2 and botocore <1.11.0 are not compatible"
_cpoolBoto3HTTPSConnection = cpool.HTTPSConnection "; please upgrade botocore (or downgrade vcrpy)"
) from e
else: else:
_Boto3VerifiedHTTPSConnection = AWSHTTPSConnection _Boto3VerifiedHTTPSConnection = AWSHTTPSConnection
_cpoolBoto3HTTPConnection = AWSHTTPConnection _cpoolBoto3HTTPConnection = AWSHTTPConnection
@@ -44,14 +44,16 @@ else:
# Try to save the original types for requests # Try to save the original types for requests
try: try:
if not cpool: import requests
import requests.packages.urllib3.connectionpool as cpool
except ImportError: # pragma: no cover except ImportError: # pragma: no cover
pass pass
else: else:
_VerifiedHTTPSConnection = cpool.VerifiedHTTPSConnection if requests.__build__ < 0x021602:
_cpoolHTTPConnection = cpool.HTTPConnection raise RuntimeError(
_cpoolHTTPSConnection = cpool.HTTPSConnection "vcrpy >=4.2.2 and requests <2.16.2 are not compatible"
"; please upgrade requests (or downgrade vcrpy)"
)
# Try to save the original types for httplib2 # Try to save the original types for httplib2
try: try:
@@ -94,6 +96,15 @@ else:
_AiohttpClientSessionRequest = aiohttp.client.ClientSession._request _AiohttpClientSessionRequest = aiohttp.client.ClientSession._request
try:
import httpx
except ImportError: # pragma: no cover
pass
else:
_HttpxSyncClient_send = httpx.Client.send
_HttpxAsyncClient_send = httpx.AsyncClient.send
class CassettePatcherBuilder: class CassettePatcherBuilder:
def _build_patchers_from_mock_triples_decorator(function): def _build_patchers_from_mock_triples_decorator(function):
@functools.wraps(function) @functools.wraps(function)
@@ -116,6 +127,7 @@ class CassettePatcherBuilder:
self._boto(), self._boto(),
self._tornado(), self._tornado(),
self._aiohttp(), self._aiohttp(),
self._httpx(),
self._build_patchers_from_mock_triples(self._cassette.custom_patches), self._build_patchers_from_mock_triples(self._cassette.custom_patches),
) )
@@ -190,20 +202,11 @@ class CassettePatcherBuilder:
@_build_patchers_from_mock_triples_decorator @_build_patchers_from_mock_triples_decorator
def _boto3(self): def _boto3(self):
try: try:
# botocore using awsrequest # botocore using awsrequest
import botocore.awsrequest as cpool import botocore.awsrequest as cpool
except ImportError: # pragma: no cover except ImportError: # pragma: no cover
try: pass
# botocore using vendored requests
import botocore.vendored.requests.packages.urllib3.connectionpool as cpool
except ImportError: # pragma: no cover
pass
else:
from .stubs import boto3_stubs
yield self._urllib3_patchers(cpool, boto3_stubs)
else: else:
from .stubs import boto3_stubs from .stubs import boto3_stubs
@@ -259,8 +262,7 @@ class CassettePatcherBuilder:
except ImportError: # pragma: no cover except ImportError: # pragma: no cover
pass pass
else: else:
from .stubs.httplib2_stubs import VCRHTTPConnectionWithTimeout from .stubs.httplib2_stubs import VCRHTTPConnectionWithTimeout, VCRHTTPSConnectionWithTimeout
from .stubs.httplib2_stubs import VCRHTTPSConnectionWithTimeout
yield cpool, "HTTPConnectionWithTimeout", VCRHTTPConnectionWithTimeout yield cpool, "HTTPConnectionWithTimeout", VCRHTTPConnectionWithTimeout
yield cpool, "HTTPSConnectionWithTimeout", VCRHTTPSConnectionWithTimeout yield cpool, "HTTPSConnectionWithTimeout", VCRHTTPSConnectionWithTimeout
@@ -313,6 +315,21 @@ class CassettePatcherBuilder:
new_request = vcr_request(self._cassette, _AiohttpClientSessionRequest) new_request = vcr_request(self._cassette, _AiohttpClientSessionRequest)
yield client.ClientSession, "_request", new_request yield client.ClientSession, "_request", new_request
@_build_patchers_from_mock_triples_decorator
def _httpx(self):
try:
import httpx
except ImportError: # pragma: no cover
return
else:
from .stubs.httpx_stubs import async_vcr_send, sync_vcr_send
new_async_client_send = async_vcr_send(self._cassette, _HttpxAsyncClient_send)
yield httpx.AsyncClient, "send", new_async_client_send
new_sync_client_send = sync_vcr_send(self._cassette, _HttpxSyncClient_send)
yield httpx.Client, "send", new_sync_client_send
def _urllib3_patchers(self, cpool, stubs): def _urllib3_patchers(self, cpool, stubs):
http_connection_remover = ConnectionRemover( http_connection_remover = ConnectionRemover(
self._get_cassette_subclass(stubs.VCRRequestsHTTPConnection) self._get_cassette_subclass(stubs.VCRRequestsHTTPConnection)
@@ -392,36 +409,6 @@ def reset_patchers():
yield mock.patch.object(httplib, "HTTPConnection", _HTTPConnection) yield mock.patch.object(httplib, "HTTPConnection", _HTTPConnection)
yield mock.patch.object(httplib, "HTTPSConnection", _HTTPSConnection) yield mock.patch.object(httplib, "HTTPSConnection", _HTTPSConnection)
try:
import requests
if requests.__build__ < 0x021603:
# Avoid double unmock if requests 2.16.3
# First, this is pointless, requests.packages.urllib3 *IS* urllib3 (see packages.py)
# Second, this is unmocking twice the same classes with different namespaces
# and is creating weird issues and bugs:
# > AssertionError: assert <class 'urllib3.connection.HTTPConnection'>
# > is <class 'requests.packages.urllib3.connection.HTTPConnection'>
# This assert should work!!!
# Note that this also means that now, requests.packages is never imported
# if requests 2.16.3 or greater is used with VCRPy.
import requests.packages.urllib3.connectionpool as cpool
else:
raise ImportError("Skip requests not vendored anymore")
except ImportError: # pragma: no cover
pass
else:
# unpatch requests v1.x
yield mock.patch.object(cpool, "VerifiedHTTPSConnection", _VerifiedHTTPSConnection)
yield mock.patch.object(cpool, "HTTPConnection", _cpoolHTTPConnection)
# unpatch requests v2.x
if hasattr(cpool.HTTPConnectionPool, "ConnectionCls"):
yield mock.patch.object(cpool.HTTPConnectionPool, "ConnectionCls", _cpoolHTTPConnection)
yield mock.patch.object(cpool.HTTPSConnectionPool, "ConnectionCls", _cpoolHTTPSConnection)
if hasattr(cpool, "HTTPSConnection"):
yield mock.patch.object(cpool, "HTTPSConnection", _cpoolHTTPSConnection)
try: try:
import urllib3.connectionpool as cpool import urllib3.connectionpool as cpool
except ImportError: # pragma: no cover except ImportError: # pragma: no cover
@@ -438,24 +425,7 @@ def reset_patchers():
# unpatch botocore with awsrequest # unpatch botocore with awsrequest
import botocore.awsrequest as cpool import botocore.awsrequest as cpool
except ImportError: # pragma: no cover except ImportError: # pragma: no cover
try: pass
# unpatch botocore with vendored requests
import botocore.vendored.requests.packages.urllib3.connectionpool as cpool
except ImportError: # pragma: no cover
pass
else:
# unpatch requests v1.x
yield mock.patch.object(cpool, "VerifiedHTTPSConnection", _Boto3VerifiedHTTPSConnection)
yield mock.patch.object(cpool, "HTTPConnection", _cpoolBoto3HTTPConnection)
# unpatch requests v2.x
if hasattr(cpool.HTTPConnectionPool, "ConnectionCls"):
yield mock.patch.object(cpool.HTTPConnectionPool, "ConnectionCls", _cpoolBoto3HTTPConnection)
yield mock.patch.object(
cpool.HTTPSConnectionPool, "ConnectionCls", _cpoolBoto3HTTPSConnection
)
if hasattr(cpool, "HTTPSConnection"):
yield mock.patch.object(cpool, "HTTPSConnection", _cpoolBoto3HTTPSConnection)
else: else:
if hasattr(cpool.AWSHTTPConnectionPool, "ConnectionCls"): if hasattr(cpool.AWSHTTPConnectionPool, "ConnectionCls"):
yield mock.patch.object(cpool.AWSHTTPConnectionPool, "ConnectionCls", _cpoolBoto3HTTPConnection) yield mock.patch.object(cpool.AWSHTTPConnectionPool, "ConnectionCls", _cpoolBoto3HTTPConnection)

View File

@@ -1,25 +1,32 @@
# .. _persister_example: # .. _persister_example:
import os from pathlib import Path
from ..serialize import serialize, deserialize
from ..serialize import deserialize, serialize
class FilesystemPersister: class FilesystemPersister:
@classmethod @classmethod
def load_cassette(cls, cassette_path, serializer): def load_cassette(cls, cassette_path, serializer):
try: cassette_path = Path(cassette_path) # if cassette path is already Path this is no operation
with open(cassette_path) as f: if not cassette_path.is_file():
cassette_content = f.read()
except OSError:
raise ValueError("Cassette not found.") raise ValueError("Cassette not found.")
cassette = deserialize(cassette_content, serializer) try:
return cassette with cassette_path.open() as f:
data = f.read()
except UnicodeEncodeError as err:
raise ValueError("Can't read Cassette, Encoding is broken") from err
return deserialize(data, serializer)
@staticmethod @staticmethod
def save_cassette(cassette_path, cassette_dict, serializer): def save_cassette(cassette_path, cassette_dict, serializer):
data = serialize(cassette_dict, serializer) data = serialize(cassette_dict, serializer)
dirname, filename = os.path.split(cassette_path) cassette_path = Path(cassette_path) # if cassette path is already Path this is no operation
if dirname and not os.path.exists(dirname):
os.makedirs(dirname) cassette_folder = cassette_path.parent
with open(cassette_path, "w") as f: if not cassette_folder.exists():
cassette_folder.mkdir(parents=True)
with cassette_path.open("w") as f:
f.write(data) f.write(data)

23
vcr/record_mode.py Normal file
View File

@@ -0,0 +1,23 @@
from enum import Enum
class RecordMode(str, Enum):
"""
Configures when VCR will record to the cassette.
Can be declared by either using the enumerated value (`vcr.mode.ONCE`)
or by simply using the defined string (`once`).
`ALL`: Every request is recorded.
`ANY`: ?
`NEW_EPISODES`: Any request not found in the cassette is recorded.
`NONE`: No requests are recorded.
`ONCE`: First set of requests is recorded, all others are replayed.
Attempting to add a new episode fails.
"""
ALL = "all"
ANY = "any"
NEW_EPISODES = "new_episodes"
NONE = "none"
ONCE = "once"

View File

@@ -1,8 +1,9 @@
import logging
import warnings import warnings
from io import BytesIO from io import BytesIO
from urllib.parse import urlparse, parse_qsl from urllib.parse import parse_qsl, urlparse
from .util import CaseInsensitiveDict from .util import CaseInsensitiveDict
import logging
log = logging.getLogger(__name__) log = logging.getLogger(__name__)

View File

@@ -1,7 +1,8 @@
from vcr.serializers import compat
from vcr.request import Request
import yaml import yaml
from vcr.request import Request
from vcr.serializers import compat
# version 1 cassettes started with VCR 1.0.x. # version 1 cassettes started with VCR 1.0.x.
# Before 1.0.x, there was no versioning. # Before 1.0.x, there was no versioning.
CASSETTE_FORMAT_VERSION = 1 CASSETTE_FORMAT_VERSION = 1

View File

@@ -16,7 +16,7 @@ def serialize(cassette_dict):
) )
try: try:
return json.dumps(cassette_dict, indent=4) return json.dumps(cassette_dict, indent=4) + "\n"
except UnicodeDecodeError as original: # py2 except UnicodeDecodeError as original: # py2
raise UnicodeDecodeError( raise UnicodeDecodeError(
original.encoding, original.encoding,

View File

@@ -2,9 +2,10 @@ import yaml
# Use the libYAML versions if possible # Use the libYAML versions if possible
try: try:
from yaml import CLoader as Loader, CDumper as Dumper from yaml import CDumper as Dumper
from yaml import CLoader as Loader
except ImportError: except ImportError:
from yaml import Loader, Dumper from yaml import Dumper, Loader
def deserialize(cassette_string): def deserialize(cassette_string):

View File

@@ -1,14 +1,12 @@
"""Stubs for patching HTTP and HTTPS requests""" """Stubs for patching HTTP and HTTPS requests"""
import logging import logging
from http.client import ( from http.client import HTTPConnection, HTTPResponse, HTTPSConnection
HTTPConnection,
HTTPSConnection,
HTTPResponse,
)
from io import BytesIO from io import BytesIO
from vcr.request import Request
from vcr.errors import CannotOverwriteExistingCassetteException from vcr.errors import CannotOverwriteExistingCassetteException
from vcr.request import Request
from . import compat from . import compat
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@@ -87,7 +85,7 @@ class VCRHTTPResponse(HTTPResponse):
def closed(self): def closed(self):
# in python3, I can't change the value of self.closed. So I' # in python3, I can't change the value of self.closed. So I'
# twiddling self._closed and using this property to shadow the real # twiddling self._closed and using this property to shadow the real
# self.closed from the superclas # self.closed from the superclass
return self._closed return self._closed
def read(self, *args, **kwargs): def read(self, *args, **kwargs):
@@ -314,7 +312,7 @@ class VCRConnection:
def __setattr__(self, name, value): def __setattr__(self, name, value):
""" """
We need to define this because any attributes that are set on the We need to define this because any attributes that are set on the
VCRConnection need to be propogated to the real connection. VCRConnection need to be propagated to the real connection.
For example, urllib3 will set certain attributes on the connection, For example, urllib3 will set certain attributes on the connection,
such as 'ssl_version'. These attributes need to get set on the real such as 'ssl_version'. These attributes need to get set on the real

View File

@@ -1,13 +1,17 @@
"""Stubs for aiohttp HTTP clients""" """Stubs for aiohttp HTTP clients"""
import asyncio import asyncio
import functools import functools
import logging
import json import json
import logging
from http.cookies import CookieError, Morsel, SimpleCookie
from typing import Mapping, Union
from aiohttp import ClientConnectionError, ClientResponse, RequestInfo, streams from aiohttp import ClientConnectionError, ClientResponse, CookieJar, RequestInfo, hdrs, streams
from multidict import CIMultiDict, CIMultiDictProxy from aiohttp.helpers import strip_auth_from_url
from multidict import CIMultiDict, CIMultiDictProxy, MultiDict
from yarl import URL from yarl import URL
from vcr.errors import CannotOverwriteExistingCassetteException
from vcr.request import Request from vcr.request import Request
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@@ -59,15 +63,27 @@ def build_response(vcr_request, vcr_response, history):
request_info = RequestInfo( request_info = RequestInfo(
url=URL(vcr_request.url), url=URL(vcr_request.url),
method=vcr_request.method, method=vcr_request.method,
headers=CIMultiDictProxy(CIMultiDict(vcr_request.headers)), headers=_deserialize_headers(vcr_request.headers),
real_url=URL(vcr_request.url), real_url=URL(vcr_request.url),
) )
response = MockClientResponse(vcr_request.method, URL(vcr_response.get("url")), request_info=request_info) response = MockClientResponse(vcr_request.method, URL(vcr_response.get("url")), request_info=request_info)
response.status = vcr_response["status"]["code"] response.status = vcr_response["status"]["code"]
response._body = vcr_response["body"].get("string", b"") response._body = vcr_response["body"].get("string", b"")
response.reason = vcr_response["status"]["message"] response.reason = vcr_response["status"]["message"]
response._headers = CIMultiDictProxy(CIMultiDict(vcr_response["headers"])) response._headers = _deserialize_headers(vcr_response["headers"])
response._history = tuple(history) response._history = tuple(history)
# cookies
for hdr in response.headers.getall(hdrs.SET_COOKIE, ()):
try:
cookies = SimpleCookie(hdr)
for cookie_name, cookie in cookies.items():
expires = cookie.get("expires", "").strip()
if expires:
log.debug('Ignoring expiration date: %s="%s"', cookie_name, expires)
cookie["expires"] = ""
response.cookies.load(cookie.output(header="").strip())
except CookieError as exc:
log.warning("Can not load response cookies: %s", exc)
response.close() response.close()
return response return response
@@ -75,31 +91,54 @@ def build_response(vcr_request, vcr_response, history):
def _serialize_headers(headers): def _serialize_headers(headers):
"""Serialize CIMultiDictProxy to a pickle-able dict because proxy """Serialize CIMultiDictProxy to a pickle-able dict because proxy
objects forbid pickling: objects forbid pickling:
https://github.com/aio-libs/multidict/issues/340 https://github.com/aio-libs/multidict/issues/340
""" """
# Mark strings as keys so 'istr' types don't show up in # Mark strings as keys so 'istr' types don't show up in
# the cassettes as comments. # the cassettes as comments.
return {str(k): v for k, v in headers.items()} serialized_headers = {}
for k, v in headers.items():
serialized_headers.setdefault(str(k), []).append(v)
return serialized_headers
def play_responses(cassette, vcr_request): def _deserialize_headers(headers):
deserialized_headers = CIMultiDict()
for k, vs in headers.items():
if isinstance(vs, list):
for v in vs:
deserialized_headers.add(k, v)
else:
deserialized_headers.add(k, vs)
return CIMultiDictProxy(deserialized_headers)
def play_responses(cassette, vcr_request, kwargs):
history = [] history = []
allow_redirects = kwargs.get("allow_redirects", True)
vcr_response = cassette.play_response(vcr_request) vcr_response = cassette.play_response(vcr_request)
response = build_response(vcr_request, vcr_response, history) response = build_response(vcr_request, vcr_response, history)
# If we're following redirects, continue playing until we reach # If we're following redirects, continue playing until we reach
# our final destination. # our final destination.
while 300 <= response.status <= 399: while allow_redirects and 300 <= response.status <= 399:
next_url = URL(response.url).with_path(response.headers["location"]) if "location" not in response.headers:
break
next_url = URL(response.url).join(URL(response.headers["location"]))
# Make a stub VCR request that we can then use to look up the recorded # Make a stub VCR request that we can then use to look up the recorded
# VCR request saved to the cassette. This feels a little hacky and # VCR request saved to the cassette. This feels a little hacky and
# may have edge cases based on the headers we're providing (e.g. if # may have edge cases based on the headers we're providing (e.g. if
# there's a matcher that is used to filter by headers). # there's a matcher that is used to filter by headers).
vcr_request = Request("GET", str(next_url), None, _serialize_headers(response.request_info.headers)) vcr_request = Request("GET", str(next_url), None, _serialize_headers(response.request_info.headers))
vcr_request = cassette.find_requests_with_most_matches(vcr_request)[0][0] vcr_requests = cassette.find_requests_with_most_matches(vcr_request)
for vcr_request, *_ in vcr_requests:
if cassette.can_play_response_for(vcr_request):
break
# Tack on the response we saw from the redirect into the history # Tack on the response we saw from the redirect into the history
# list that is added on to the final response. # list that is added on to the final response.
@@ -132,19 +171,19 @@ async def record_response(cassette, vcr_request, response):
async def record_responses(cassette, vcr_request, response): async def record_responses(cassette, vcr_request, response):
"""Because aiohttp follows redirects by default, we must support """Because aiohttp follows redirects by default, we must support
them by default. This method is used to write individual them by default. This method is used to write individual
request-response chains that were implicitly followed to get request-response chains that were implicitly followed to get
to the final destination. to the final destination.
""" """
for past_response in response.history: for i, past_response in enumerate(response.history):
aiohttp_request = past_response.request_info aiohttp_request = past_response.request_info
# No data because it's following a redirect.
past_request = Request( past_request = Request(
aiohttp_request.method, aiohttp_request.method,
str(aiohttp_request.url), str(aiohttp_request.url),
None, # Record body of first request, rest are following a redirect.
None if i else vcr_request.body,
_serialize_headers(aiohttp_request.headers), _serialize_headers(aiohttp_request.headers),
) )
await record_response(cassette, past_request, past_response) await record_response(cassette, past_request, past_response)
@@ -163,6 +202,43 @@ async def record_responses(cassette, vcr_request, response):
await record_response(cassette, vcr_request, response) await record_response(cassette, vcr_request, response)
def _build_cookie_header(session, cookies, cookie_header, url):
url, _ = strip_auth_from_url(url)
all_cookies = session._cookie_jar.filter_cookies(url)
if cookies is not None:
tmp_cookie_jar = CookieJar()
tmp_cookie_jar.update_cookies(cookies)
req_cookies = tmp_cookie_jar.filter_cookies(url)
if req_cookies:
all_cookies.load(req_cookies)
if not all_cookies and not cookie_header:
return None
c = SimpleCookie()
if cookie_header:
c.load(cookie_header)
for name, value in all_cookies.items():
if isinstance(value, Morsel):
mrsl_val = value.get(value.key, Morsel())
mrsl_val.set(value.key, value.value, value.coded_value)
c[name] = mrsl_val
else:
c[name] = value
return c.output(header="", sep=";").strip()
def _build_url_with_params(url_str: str, params: Mapping[str, Union[str, int, float]]) -> URL:
# This code is basically a copy&paste of aiohttp.
# https://github.com/aio-libs/aiohttp/blob/master/aiohttp/client_reqrep.py#L225
url = URL(url_str)
q = MultiDict(url.query)
url2 = url.with_query(params)
q.extend(url2.query)
return url.with_query(q)
def vcr_request(cassette, real_request): def vcr_request(cassette, real_request):
@functools.wraps(real_request) @functools.wraps(real_request)
async def new_request(self, method, url, **kwargs): async def new_request(self, method, url, **kwargs):
@@ -171,32 +247,29 @@ def vcr_request(cassette, real_request):
headers = self._prepare_headers(headers) headers = self._prepare_headers(headers)
data = kwargs.get("data", kwargs.get("json")) data = kwargs.get("data", kwargs.get("json"))
params = kwargs.get("params") params = kwargs.get("params")
cookies = kwargs.get("cookies")
if auth is not None: if auth is not None:
headers["AUTHORIZATION"] = auth.encode() headers["AUTHORIZATION"] = auth.encode()
request_url = URL(url) request_url = URL(url) if not params else _build_url_with_params(url, params)
if params: c_header = headers.pop(hdrs.COOKIE, None)
for k, v in params.items(): cookie_header = _build_cookie_header(self, cookies, c_header, request_url)
params[k] = str(v) if cookie_header:
request_url = URL(url).with_query(params) headers[hdrs.COOKIE] = cookie_header
vcr_request = Request(method, str(request_url), data, headers) vcr_request = Request(method, str(request_url), data, _serialize_headers(headers))
if cassette.can_play_response_for(vcr_request): if cassette.can_play_response_for(vcr_request):
return play_responses(cassette, vcr_request) log.info("Playing response for {} from cassette".format(vcr_request))
response = play_responses(cassette, vcr_request, kwargs)
for redirect in response.history:
self._cookie_jar.update_cookies(redirect.cookies, redirect.url)
self._cookie_jar.update_cookies(response.cookies, response.url)
return response
if cassette.write_protected and cassette.filter_request(vcr_request): if cassette.write_protected and cassette.filter_request(vcr_request):
response = MockClientResponse(method, URL(url)) raise CannotOverwriteExistingCassetteException(cassette=cassette, failed_request=vcr_request)
response.status = 599
msg = (
"No match for the request {!r} was found. Can't overwrite "
"existing cassette {!r} in your current record mode {!r}."
)
msg = msg.format(vcr_request, cassette._path, cassette.record_mode)
response._body = msg.encode()
response.close()
return response
log.info("%s not in cassette, sending to real server", vcr_request) log.info("%s not in cassette, sending to real server", vcr_request)

View File

@@ -1,17 +1,6 @@
"""Stubs for boto3""" """Stubs for boto3"""
try: from botocore.awsrequest import AWSHTTPConnection as HTTPConnection
# boto using awsrequest from botocore.awsrequest import AWSHTTPSConnection as VerifiedHTTPSConnection
from botocore.awsrequest import AWSHTTPConnection as HTTPConnection
from botocore.awsrequest import AWSHTTPSConnection as VerifiedHTTPSConnection
except ImportError: # pragma: nocover
# boto using vendored requests
# urllib3 defines its own HTTPConnection classes, which boto3 goes ahead and assumes
# you're using. It includes some polyfills for newer features missing in older pythons.
try:
from urllib3.connectionpool import HTTPConnection, VerifiedHTTPSConnection
except ImportError: # pragma: nocover
from requests.packages.urllib3.connectionpool import HTTPConnection, VerifiedHTTPSConnection
from ..stubs import VCRHTTPConnection, VCRHTTPSConnection from ..stubs import VCRHTTPConnection, VCRHTTPSConnection

View File

@@ -1,6 +1,7 @@
"""Stubs for boto""" """Stubs for boto"""
from boto.https_connection import CertValidatingHTTPSConnection from boto.https_connection import CertValidatingHTTPSConnection
from ..stubs import VCRHTTPSConnection from ..stubs import VCRHTTPSConnection

View File

@@ -1,6 +1,5 @@
from io import BytesIO
import http.client import http.client
from io import BytesIO
""" """
The python3 http.client api moved some stuff around, so this is an abstraction The python3 http.client api moved some stuff around, so this is an abstraction
@@ -13,7 +12,7 @@ def get_header(message, name):
def get_header_items(message): def get_header_items(message):
for (key, values) in get_headers(message): for key, values in get_headers(message):
for value in values: for value in values:
yield key, value yield key, value

View File

@@ -1,6 +1,7 @@
"""Stubs for httplib2""" """Stubs for httplib2"""
from httplib2 import HTTPConnectionWithTimeout, HTTPSConnectionWithTimeout from httplib2 import HTTPConnectionWithTimeout, HTTPSConnectionWithTimeout
from ..stubs import VCRHTTPConnection, VCRHTTPSConnection from ..stubs import VCRHTTPConnection, VCRHTTPSConnection
@@ -27,7 +28,6 @@ class VCRHTTPSConnectionWithTimeout(VCRHTTPSConnection, HTTPSConnectionWithTimeo
_baseclass = HTTPSConnectionWithTimeout _baseclass = HTTPSConnectionWithTimeout
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
# Delete the keyword arguments that HTTPSConnection would not recognize # Delete the keyword arguments that HTTPSConnection would not recognize
safe_keys = { safe_keys = {
"host", "host",

171
vcr/stubs/httpx_stubs.py Normal file
View File

@@ -0,0 +1,171 @@
import functools
import inspect
import logging
from unittest.mock import MagicMock, patch
import httpx
from vcr.errors import CannotOverwriteExistingCassetteException
from vcr.request import Request as VcrRequest
_httpx_signature = inspect.signature(httpx.Client.request)
try:
HTTPX_REDIRECT_PARAM = _httpx_signature.parameters["follow_redirects"]
except KeyError:
HTTPX_REDIRECT_PARAM = _httpx_signature.parameters["allow_redirects"]
_logger = logging.getLogger(__name__)
def _transform_headers(httpx_response):
"""
Some headers can appear multiple times, like "Set-Cookie".
Therefore transform to every header key to list of values.
"""
out = {}
for key, var in httpx_response.headers.raw:
decoded_key = key.decode("utf-8")
out.setdefault(decoded_key, [])
out[decoded_key].append(var.decode("utf-8"))
return out
def _to_serialized_response(httpx_response):
return {
"status_code": httpx_response.status_code,
"http_version": httpx_response.http_version,
"headers": _transform_headers(httpx_response),
"content": httpx_response.content.decode("utf-8", "ignore"),
}
def _from_serialized_headers(headers):
"""
httpx accepts headers as list of tuples of header key and value.
"""
header_list = []
for key, values in headers.items():
for v in values:
header_list.append((key, v))
return header_list
@patch("httpx.Response.close", MagicMock())
@patch("httpx.Response.read", MagicMock())
def _from_serialized_response(request, serialized_response, history=None):
content = serialized_response.get("content").encode()
response = httpx.Response(
status_code=serialized_response.get("status_code"),
request=request,
headers=_from_serialized_headers(serialized_response.get("headers")),
content=content,
history=history or [],
)
response._content = content
return response
def _make_vcr_request(httpx_request, **kwargs):
body = httpx_request.read().decode("utf-8")
uri = str(httpx_request.url)
headers = dict(httpx_request.headers)
return VcrRequest(httpx_request.method, uri, body, headers)
def _shared_vcr_send(cassette, real_send, *args, **kwargs):
real_request = args[1]
vcr_request = _make_vcr_request(real_request, **kwargs)
if cassette.can_play_response_for(vcr_request):
return vcr_request, _play_responses(cassette, real_request, vcr_request, args[0], kwargs)
if cassette.write_protected and cassette.filter_request(vcr_request):
raise CannotOverwriteExistingCassetteException(cassette=cassette, failed_request=vcr_request)
_logger.info("%s not in cassette, sending to real server", vcr_request)
return vcr_request, None
def _record_responses(cassette, vcr_request, real_response):
for past_real_response in real_response.history:
past_vcr_request = _make_vcr_request(past_real_response.request)
cassette.append(past_vcr_request, _to_serialized_response(past_real_response))
if real_response.history:
# If there was a redirection keep we want the request which will hold the
# final redirect value
vcr_request = _make_vcr_request(real_response.request)
cassette.append(vcr_request, _to_serialized_response(real_response))
return real_response
def _play_responses(cassette, request, vcr_request, client, kwargs):
history = []
allow_redirects = kwargs.get(
HTTPX_REDIRECT_PARAM.name,
HTTPX_REDIRECT_PARAM.default,
)
vcr_response = cassette.play_response(vcr_request)
response = _from_serialized_response(request, vcr_response)
while allow_redirects and 300 <= response.status_code <= 399:
next_url = response.headers.get("location")
if not next_url:
break
vcr_request = VcrRequest("GET", next_url, None, dict(response.headers))
vcr_request = cassette.find_requests_with_most_matches(vcr_request)[0][0]
history.append(response)
# add cookies from response to session cookie store
client.cookies.extract_cookies(response)
vcr_response = cassette.play_response(vcr_request)
response = _from_serialized_response(vcr_request, vcr_response, history)
return response
async def _async_vcr_send(cassette, real_send, *args, **kwargs):
vcr_request, response = _shared_vcr_send(cassette, real_send, *args, **kwargs)
if response:
# add cookies from response to session cookie store
args[0].cookies.extract_cookies(response)
return response
real_response = await real_send(*args, **kwargs)
return _record_responses(cassette, vcr_request, real_response)
def async_vcr_send(cassette, real_send):
@functools.wraps(real_send)
def _inner_send(*args, **kwargs):
return _async_vcr_send(cassette, real_send, *args, **kwargs)
return _inner_send
def _sync_vcr_send(cassette, real_send, *args, **kwargs):
vcr_request, response = _shared_vcr_send(cassette, real_send, *args, **kwargs)
if response:
# add cookies from response to session cookie store
args[0].cookies.extract_cookies(response)
return response
real_response = real_send(*args, **kwargs)
return _record_responses(cassette, vcr_request, real_response)
def sync_vcr_send(cassette, real_send):
@functools.wraps(real_send)
def _inner_send(*args, **kwargs):
return _sync_vcr_send(cassette, real_send, *args, **kwargs)
return _inner_send

View File

@@ -1,9 +1,6 @@
"""Stubs for requests""" """Stubs for requests"""
try: from urllib3.connectionpool import HTTPConnection, VerifiedHTTPSConnection
from urllib3.connectionpool import HTTPConnection, VerifiedHTTPSConnection
except ImportError:
from requests.packages.urllib3.connectionpool import HTTPConnection, VerifiedHTTPSConnection
from ..stubs import VCRHTTPConnection, VCRHTTPSConnection from ..stubs import VCRHTTPConnection, VCRHTTPSConnection

View File

@@ -1,6 +1,7 @@
"""Stubs for urllib3""" """Stubs for urllib3"""
from urllib3.connectionpool import HTTPConnection, VerifiedHTTPSConnection from urllib3.connectionpool import HTTPConnection, VerifiedHTTPSConnection
from ..stubs import VCRHTTPConnection, VCRHTTPSConnection from ..stubs import VCRHTTPConnection, VCRHTTPSConnection
# urllib3 defines its own HTTPConnection classes. It includes some polyfills # urllib3 defines its own HTTPConnection classes. It includes some polyfills