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

Compare commits

..

92 Commits

Author SHA1 Message Date
Kevin McCarthy
d2281ab646 version bump to v4.4.0 2023-06-26 09:17:41 -05:00
Sebastian Pipping
8b97fd6551 Merge pull request #644 from neliseiska/replace_assert_with_raise
Replace `assert` with `raise AssertionError`
2023-06-22 22:29:13 +02:00
Sebastian Pipping
29e42211d7 Merge pull request #722 from kevin1024/run-online-tests-only-once
main.yml: Run online tests only once (to save runtime)
2023-06-22 15:46:04 +02:00
Sebastian Pipping
6e511b67fd Merge pull request #723 from kevin1024/issue-719-compression-urllib3-v2
Make decompression robust towards already decompressed input (arguably fixes #719)
2023-06-22 15:45:10 +02:00
Sebastian Pipping
9b6cb1ce23 Merge pull request #721 from kevin1024/issue-714-response-raw-stream-urllib3-v2
Make response.raw.stream() work for urllib3 v2 (fixes #714)
2023-06-22 15:44:29 +02:00
Sebastian Pipping
6a12bd1511 test_requests.py: Cover response.raw.stream() 2023-06-21 14:52:13 +02:00
Sebastian Pipping
3411bedc06 Make response.raw.stream() work for urllib3 v2 2023-06-21 14:52:13 +02:00
Sebastian Pipping
438a65426b filters.py: Make decompression robust towards decompressed input 2023-06-21 02:28:36 +02:00
Sebastian Pipping
8c6b1fdf38 test_requests.py: Extend coverage of gzip response
.. with regard to:
- not crashing with decode_compressed_response==True
- expected cassette content for body string
- expected response content, i.e. proper decompression
2023-06-21 02:28:36 +02:00
Sebastian Pipping
15e9f1868c main.yml: Run online tests only once
Online tests are tests that need access to the Internet
to pass (and hence have @pytest.marker.online decoration).
2023-06-21 00:38:58 +02:00
Sebastian Pipping
7eb235cd9c Merge pull request #720 from kevin1024/use-python3-command
Use python3 (and pip3) command
2023-06-19 15:07:23 +02:00
Sebastian Pipping
d2f2731481 Replace command "pip" with "pip3" 2023-06-18 23:08:17 +02:00
Sebastian Pipping
b2a895cb89 Replace command "python" by "python3" 2023-06-18 23:07:04 +02:00
Sebastian Pipping
ffb2f44236 Merge pull request #718 from kevin1024/enforce-online-marker-completeness
Make CI enforce that all online tests are marked with @pytest.mark.online
2023-06-18 23:03:48 +02:00
Sebastian Pipping
d66392a3fb main.yml: Enforce that use of @pytest.mark.online remains complete 2023-06-18 21:33:30 +02:00
Sebastian Pipping
b9cab239a7 runtests.sh: Fix variable quoting + add exec 2023-06-18 17:40:49 +02:00
Sebastian Pipping
276a41d9b6 Merge pull request #674 from jspricke/pytest.mark.online
Mark tests with @pytest.mark.online that need access to the Internet
2023-06-18 17:34:16 +02:00
Jochen Sprickerhof
7007e944ae pytest.mark.online tests that need internet 2023-06-18 16:52:51 +02:00
Sebastian Pipping
bd112a2385 docs/usage.rst: Fix assertions
Symptom was:
> Traceback (most recent call last):
>   File "/tmp/tmp.kJAKlLngAX/foo.py", line 6, in <module>
>     assert 'Example domains' in response
> TypeError: a bytes-like object is required, not 'str'
2023-06-18 11:14:44 -03:00
Sebastian Pipping
42848285a0 docs/usage.rst: Fix urllib import
Symptom was:
> Traceback (most recent call last):
>   File "/tmp/tmp.kJAKlLngAX/foo.py", line 5, in <module>
>     response = urllib.request.urlopen('http://www.iana.org/domains/reserved').read()
> AttributeError: module 'urllib' has no attribute 'request'
2023-06-18 11:14:44 -03:00
Sebastian Pipping
e3aae34ef7 Merge pull request #713 from mghantous/mg/read1
VCRHTTPResponse Not Working with Biopython 1.81
2023-06-12 13:08:00 +02:00
Sebastian Pipping
f4316d2dae Merge pull request #712 from kevin1024/integrate-vcrpy-unittest
Integrate vcrpy-unittest (alternative to #709)
2023-06-08 18:09:22 +02:00
Sebastian Pipping
d613a814d3 vcr/unittest: Simplify file layout
.. and make "from vcr.unittest import VCRTestCase" work again.
2023-06-08 16:28:34 +02:00
Sebastian Pipping
ce234e503f docs/usage.rst: Drop needless self-reference 2023-06-08 16:28:34 +02:00
Sebastian Pipping
3527d25ce8 vcr/unittest: Simplify super(C, self) in class C to super()
.. for Python 3
2023-06-08 16:28:34 +02:00
Sebastian Pipping
dedb7ec403 Resolve needless inheritence from object (Python 3) 2023-06-08 16:28:34 +02:00
Sebastian Pipping
59263d6025 vcr/unittest: Resolve needless inheritence from object 2023-06-08 16:28:34 +02:00
Sebastian Pipping
2842cabec6 vcr/unittest: Remove unused logger 2023-06-08 16:28:34 +02:00
Sebastian Pipping
ad650a7ee1 vcr/unittest: Apply black formatting 2023-06-08 16:28:34 +02:00
Sebastian Pipping
9232915885 docs/usage.rst: Break up a long line 2023-06-08 16:28:34 +02:00
Sebastian Pipping
cbb540029f docs/usage.rst: Adapt documentation to new code location 2023-06-08 16:28:34 +02:00
Sebastian Pipping
bf30d9a5e5 vcr/unittest: Fix test test_get_vcr_with_matcher
Matcher needs attribute __name__ for function vcr.matchers.get_matchers_results .
2023-06-08 16:28:34 +02:00
Sebastian Pipping
f06f71ece4 vcr/unittest: Stop disguising MagicMock as Mock 2023-06-08 16:28:34 +02:00
Sebastian Pipping
1070d417b3 vcr/unittest: Apply 2to3 2023-06-08 16:28:34 +02:00
Sebastian Pipping
46726a9a61 vcr/unittest: Fix import of VCRTestCase in tests 2023-06-08 16:28:34 +02:00
Sebastian Pipping
87db8e69ff vcr/unittest: Use unitest.mock rather than mock of PyPI 2023-06-08 16:28:34 +02:00
Sebastian Pipping
52701ebca4 vcr/unittest: Make import to vcrpy relative 2023-06-08 16:28:34 +02:00
Sebastian Pipping
69679dc3fc vcr/unittest: Drop forward imports
.. to resolve import ambiguity.
2023-06-08 16:28:34 +02:00
Sebastian Pipping
c13f33b1e0 Add unmodified vcrpy-unittest code
Source commit is a2fd7625fde1ea15c8982759b07007aef40424b3.
License is MIT just like vcrpy.
2023-06-08 16:28:34 +02:00
Matt Ghantous
5476dd010c Casting to BufferedReader no longer needed in test 2023-06-05 23:47:47 -04:00
Matt Ghantous
0add77d5ae Add read1 method to VCRHTTPResponse 2023-06-05 23:20:52 -04:00
Yaroslav Halchenko
96a6e91def Codespell: action + config (#704) 2023-06-05 16:56:55 +02:00
Abram Clark
3b41f0ede3 Fix for #174 to prevent filters from corrupting request 2023-05-27 09:40:53 -03:00
Kevin McCarthy
0e06836908 bump version to v4.3.1 2023-05-26 11:02:14 -05:00
Sebastian Pipping
69db5c936f Limit support for urllib3 >=2 to Python >=3.10 for now
.. because it turned out broken for Python <3.10.
2023-05-26 10:55:44 -05:00
Sebastian Pipping
7c402ae4b0 test_vcr.py: Clarify that test_vcr_before_record_request_params is an offline test 2023-05-26 10:55:44 -05:00
Sebastian Pipping
b5c0938d2e tox.ini: Cover both urllib3 v1 and v2 2023-05-26 10:55:44 -05:00
Sebastian Pipping
3ad93fff42 tox.ini: Drop needless "boto3: urllib3"
boto3 depends on botocore which in turn depends on urllib3.
2023-05-26 10:55:44 -05:00
Sebastian Pipping
89f2005250 Fix VCRHTTPResponse for requests.cookies.extract_cookies_to_jar 2023-05-26 10:55:44 -05:00
Sebastian Pipping
88c0039089 Make test "test_cookies" more mean and helpful 2023-05-26 10:55:44 -05:00
Sebastian Pipping
1b3a1235f2 Make VCRHTTPResponse interface satisfy urllib3.response.HTTPResponse 2023-05-26 10:55:44 -05:00
Sebastian Pipping
fd1aaab3bf Respect urllib3.response.HTTPResponse.data 2023-05-26 10:55:44 -05:00
Sebastian Pipping
00da5ac5af Make test_headers robust with regard to order of headers 2023-05-26 10:55:44 -05:00
Sebastian Pipping
ac20cd1dd3 Tolerate urllib3.response.HTTPResponse.msg being None 2023-05-26 10:55:44 -05:00
Sonny V
64d6811eda build(tox.ini): revert pinning urllib to <2
In #690 a quick fix was introduced to get a green ci, this change should no longer be required.
2023-05-26 10:55:44 -05:00
Sonny V
51c99bb9df fix: use urllib3.connection where needed.
Since urllib3 v2 the re-export of connection.HTTPConnection in
urllib3.connectionpool was removed.

In this commit we use urllib3.connection where needed. Some references
to connectionpool.HTTPConnection are still there for backward
compatibility.

Closes #688
2023-05-26 10:55:44 -05:00
Sebastian Pipping
43484e7cff test_aiohttp.py: Make cookie tests use pytest-httpbin (#706)
.. to make them faster and more robust.
2023-05-26 01:16:20 +02:00
Sebastian Pipping
199f9f07f8 Merge pull request #705 from kevin1024/fix-test-dependencies
Fix test dependencies
2023-05-25 20:29:37 +02:00
Sebastian Pipping
13af8cae43 setup.py: Add missing test dependencies 2023-05-25 16:40:42 +02:00
Sebastian Pipping
436b62f587 setup.py: Drop unused test dependency "mock"
All imports use unittest.mock rather than mock of PyPI.
2023-05-25 16:33:14 +02:00
Sebastian Pipping
5b40a67b3b setup.py: Extract variable tests_require
.. and apply sorting, but nothing more
2023-05-25 16:32:08 +02:00
Sebastian Pipping
c41bd2bb40 Stop installing libgnutls28-dev 2023-05-24 16:41:46 -03:00
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
Haapalainen, Jonne
a79356cf5f Replace assert with raise AssertionError
Assert seems to behave badly in some cases and should not be used for
handling runtime errors.

see for example:
https://medium.com/@jadhavmanoj/python-what-is-raise-and-assert-statement-c3908697bc62
https://github.com/emre/notes/blob/master/python/when-to-use-assert.md
2022-05-05 11:21:27 +03:00
70 changed files with 1106 additions and 473 deletions

22
.github/workflows/codespell.yml vendored Normal file
View File

@@ -0,0 +1,22 @@
---
name: Codespell
on:
push:
branches: [master]
pull_request:
branches: [master]
permissions:
contents: read
jobs:
codespell:
name: Check for spelling errors
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Codespell
uses: codespell-project/actions-codespell@v2

View File

@@ -5,23 +5,18 @@ on:
branches:
- master
pull_request:
branches:
- "*"
workflow_dispatch:
jobs:
build:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: ["3.7", "3.8", "3.9", "3.10", "pypy-3.8"]
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.0.2
- uses: actions/checkout@v3.5.2
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
@@ -30,11 +25,19 @@ jobs:
- name: Install project dependencies
run: |
pip install --upgrade pip
pip install codecov tox tox-gh-actions
pip3 install --upgrade pip
pip3 install codecov tox tox-gh-actions
- name: Run tests with tox
run: tox
- name: Run online tests with tox
run: tox -- -m online
- name: Run offline tests with tox with no access to the Internet
run: |
# We're using unshare to take Internet access
# away from tox so that we'll notice whenever some new test
# is missing @pytest.mark.online decoration in the future
unshare --map-root-user --net -- \
sh -c 'ip link set lo up; tox -- -m "not online"'
- name: Run coverage
run: codecov

View File

@@ -71,7 +71,7 @@ Finally, register your class with VCR to use your new serializer.
import vcr
class BogoSerializer(object):
class BogoSerializer:
"""
Must implement serialize() and deserialize() methods
"""
@@ -404,3 +404,25 @@ the Cassette ``allow_playback_repeats`` option.
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

@@ -7,6 +7,29 @@ For a full list of triaged issues, bugs and PRs and what release they are target
All help in providing PRs to close out bug issues is appreciated. Even if that is providing a repo that fully replicates issues. We have very generous contributors that have added these to bug issues which meant another contributor picked up the bug and closed it out.
- 4.4.0
- HUGE thanks to @hartwork for all the work done on this release!
- Bring vcr/unittest in to vcrpy as a full feature of vcr instead of a separate library. Big thanks to @hartwork for doing this and to @agriffis for originally creating the library
- Make decompression robust towards already decompressed input (thanks @hartwork)
- Bugfix: Add read1 method (fixes compatability with biopython), thanks @mghantous
- Bugfix: Prevent filters from corrupting request (thanks @abramclark)
- Bugfix: Add support for `response.raw.stream()` to fix urllib v2 compat
- Bugfix: Replace `assert` with `raise AssertionError`: fixes support for `PYTHONOPTIMIZE=1`
- Add pytest.mark.online to run test suite offline, thanks @jspricke
- use python3 and pip3 binaries to ease debian packaging (thanks @hartwork)
- Add codespell (thanks @mghantous)
- 4.3.1
- Support urllib3 v1 and v2. NOTE: there is an issue running urllib3 v2 on
Python older than 3.10, so this is currently blocked in the requirements.
Hopefully we can resolve this situation in the future. Thanks to @shifqu,
hartwork, jairhenrique, pquentin, and vEpiphyte for your work on this.
- 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

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.
# 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
# non-false value, then it is used:

View File

@@ -74,7 +74,7 @@ The PR reviewer is a second set of eyes to see if:
**Release Manager:**
- Ensure CI is passing.
- Create a release on github and tag it with the changelog release notes.
- ``python setup.py build sdist bdist_wheel``
- ``python3 setup.py build sdist bdist_wheel``
- ``twine upload dist/*``
- Go to ReadTheDocs build page and trigger a build https://readthedocs.org/projects/vcrpy/builds/
@@ -127,7 +127,7 @@ in this example::
eval "$(pyenv init -)"
# Setup your local system tox tooling
pip install tox tox-pyenv
pip3 install tox tox-pyenv
# Install supported versions (at time of writing), this does not activate them
pyenv install 3.7.5 3.8.0 pypy3.8

View File

@@ -4,7 +4,7 @@ Installation
VCR.py is a package on `PyPI <https://pypi.python.org>`__, so you can install
with pip::
pip install vcrpy
pip3 install vcrpy
Compatibility
-------------
@@ -35,7 +35,7 @@ rebuilding pyyaml.
1. Test if pyyaml is built with libyaml. This should work::
python -c 'from yaml import CLoader'
python3 -c 'from yaml import CLoader'
2. Install libyaml according to your Linux distribution, or using `Homebrew
<http://mxcl.github.com/homebrew/>`__ on Mac::
@@ -46,8 +46,8 @@ rebuilding pyyaml.
3. Rebuild pyyaml with libyaml::
pip uninstall pyyaml
pip --no-cache-dir install pyyaml
pip3 uninstall pyyaml
pip3 --no-cache-dir install pyyaml
Upgrade
-------
@@ -61,7 +61,7 @@ is to simply delete your cassettes and re-record all of them. VCR.py
also provides a migration script that attempts to upgrade your 0.x
cassettes to the new 1.x format. To use it, run the following command::
python -m vcr.migration PATH
python3 -m vcr.migration PATH
The PATH can be either a path to the directory with cassettes or the
path to a single cassette.

View File

@@ -4,11 +4,11 @@ Usage
.. code:: python
import vcr
import urllib
import urllib.request
with vcr.use_cassette('fixtures/vcr_cassettes/synopsis.yaml'):
response = urllib.request.urlopen('http://www.iana.org/domains/reserved').read()
assert 'Example domains' in response
assert b'Example domains' in response
Run this test once, and VCR.py will record the HTTP request to
``fixtures/vcr_cassettes/synopsis.yaml``. Run it again, and VCR.py will
@@ -26,7 +26,7 @@ look like this:
@vcr.use_cassette('fixtures/vcr_cassettes/synopsis.yaml')
def test_iana():
response = urllib.request.urlopen('http://www.iana.org/domains/reserved').read()
assert 'Example domains' in response
assert b'Example domains' in response
When using the decorator version of ``use_cassette``, it is possible to
omit the path to the cassette file.
@@ -36,7 +36,7 @@ omit the path to the cassette file.
@vcr.use_cassette()
def test_iana():
response = urllib.request.urlopen('http://www.iana.org/domains/reserved').read()
assert 'Example domains' in response
assert b'Example domains' in response
In this case, the cassette file will be given the same name as the test
function, and it will be placed in the same directory as the file in
@@ -92,9 +92,73 @@ all
Unittest Integration
--------------------
While it's possible to use the context manager or decorator forms with unittest,
there's also a ``VCRTestCase`` provided separately by `vcrpy-unittest
<https://github.com/agriffis/vcrpy-unittest>`__.
Inherit from ``VCRTestCase`` for automatic recording and playback of HTTP
interactions.
.. code:: python
from vcr.unittest import VCRTestCase
import requests
class MyTestCase(VCRTestCase):
def test_something(self):
response = requests.get('http://example.com')
Similar to how VCR.py returns the cassette from the context manager,
``VCRTestCase`` makes the cassette available as ``self.cassette``:
.. code:: python
self.assertEqual(len(self.cassette), 1)
self.assertEqual(self.cassette.requests[0].uri, 'http://example.com')
By default cassettes will be placed in the ``cassettes`` subdirectory next to the
test, named according to the test class and method. For example, the above test
would read from and write to ``cassettes/MyTestCase.test_something.yaml``
The configuration can be modified by overriding methods on your subclass:
``_get_vcr_kwargs``, ``_get_cassette_library_dir`` and ``_get_cassette_name``.
To modify the ``VCR`` object after instantiation, for example to add a matcher,
you can hook on ``_get_vcr``, for example:
.. code:: python
class MyTestCase(VCRTestCase):
def _get_vcr(self, **kwargs):
myvcr = super(MyTestCase, self)._get_vcr(**kwargs)
myvcr.register_matcher('mymatcher', mymatcher)
myvcr.match_on = ['mymatcher']
return myvcr
See
`the source
<https://github.com/kevin1024/vcrpy/blob/master/vcr/unittest.py>`__
for the default implementations of these methods.
If you implement a ``setUp`` method on your test class then make sure to call
the parent version ``super().setUp()`` in your own in order to continue getting
the cassettes produced.
VCRMixin
~~~~~~~~
In case inheriting from ``VCRTestCase`` is difficult because of an existing
class hierarchy containing tests in the base classes, inherit from ``VCRMixin``
instead.
.. code:: python
from vcr.unittest import VCRMixin
import requests
import unittest
class MyTestMixin(VCRMixin):
def test_something(self):
response = requests.get(self.url)
class MyTestCase(MyTestMixin, unittest.TestCase):
url = 'http://example.com'
Pytest Integration
------------------

View File

@@ -1,2 +1,20 @@
[tool.black]
line-length=110
[tool.isort]
line_length = 110
known_first_party = "vcrpy"
multi_line_output = 3
use_parentheses = true
include_trailing_comma = true
[tool.codespell]
skip = '.git,*.pdf,*.svg,.tox'
ignore-regex = "\\\\[fnrstv]"
#
# ignore-words-list = ''
[tool.pytest.ini_options]
markers = [
"online",
]

View File

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

View File

@@ -5,7 +5,7 @@ import os
import re
import sys
from setuptools import setup, find_packages
from setuptools import find_packages, setup
from setuptools.command.test import test as TestCommand
long_description = open("README.rst", "r").read()
@@ -47,6 +47,32 @@ install_requires = [
"wrapt",
"six>=1.5",
"yarl",
# Support for urllib3 >=2 needs Python >=3.10
# so we need to block urllib3 >=2 for Python <3.10 for now.
# Note that vcrpy would work fine without any urllib3 around,
# so this block and the dependency can be dropped at some point
# in the future. For more Details:
# https://github.com/kevin1024/vcrpy/pull/699#issuecomment-1551439663
"urllib3 <2; python_version <'3.10'",
]
tests_require = [
"aiohttp",
"boto3",
"httplib2",
"httpx",
"pytest",
"pytest-aiohttp",
"pytest-httpbin",
"requests",
"tornado",
# Needed to un-break httpbin 0.7.0. For httpbin >=0.7.1 and after,
# this pin and the dependency itself can be removed, provided
# that the related bug in httpbin has been fixed:
# https://github.com/kevin1024/vcrpy/issues/645#issuecomment-1562489489
# https://github.com/postmanlabs/httpbin/issues/673
# https://github.com/postmanlabs/httpbin/pull/674
"Werkzeug==2.0.3",
]
setup(
@@ -62,7 +88,7 @@ setup(
python_requires=">=3.7",
install_requires=install_requires,
license="MIT",
tests_require=["pytest", "mock", "pytest-httpbin"],
tests_require=tests_require,
classifiers=[
"Development Status :: 5 - Production/Stable",
"Environment :: Console",
@@ -73,6 +99,7 @@ setup(
"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 :: Implementation :: CPython",
"Programming Language :: Python :: Implementation :: PyPy",

View File

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

View File

@@ -13,7 +13,7 @@ interactions:
user-agent:
- python-httpx/0.12.1
method: GET
uri: https://httpbin.org/headers
uri: https://mockbin.org/headers
response:
content: "{\n \"headers\": {\n \"Accept\": \"*/*\", \n \"Accept-Encoding\"\
: \"gzip, deflate, br\", \n \"Host\": \"httpbin.org\", \n \"User-Agent\"\

View File

@@ -0,0 +1,32 @@
import os
import ssl
import pytest
@pytest.fixture(params=["https", "http"])
def scheme(request):
"""Fixture that returns both http and https."""
return request.param
@pytest.fixture
def mockbin(scheme):
return scheme + "://mockbin.org"
@pytest.fixture
def mockbin_request_url(mockbin):
return mockbin + "/request"
@pytest.fixture
def httpbin_ssl_context():
ssl_ca_location = os.environ["REQUESTS_CA_BUNDLE"]
ssl_cert_location = os.environ["REQUESTS_CA_BUNDLE"].replace("cacert.pem", "cert.pem")
ssl_key_location = os.environ["REQUESTS_CA_BUNDLE"].replace("cacert.pem", "key.pem")
ssl_context = ssl.create_default_context(cafile=ssl_ca_location)
ssl_context.load_cert_chain(ssl_cert_location, ssl_key_location)
return ssl_context

View File

@@ -8,6 +8,7 @@ asyncio = pytest.importorskip("asyncio")
aiohttp = pytest.importorskip("aiohttp")
import vcr # noqa: E402
from .aiohttp_utils import aiohttp_app, aiohttp_request # noqa: E402
@@ -33,14 +34,10 @@ def post(url, output="text", **kwargs):
return request("POST", url, output="text", **kwargs)
@pytest.fixture(params=["https", "http"])
def scheme(request):
"""Fixture that returns both http and https."""
return request.param
@pytest.mark.online
def test_status(tmpdir, mockbin_request_url):
url = mockbin_request_url
def test_status(tmpdir, scheme):
url = scheme + "://httpbin.org"
with vcr.use_cassette(str(tmpdir.join("status.yaml"))):
response, _ = get(url)
@@ -50,9 +47,10 @@ def test_status(tmpdir, scheme):
assert cassette.play_count == 1
@pytest.mark.online
@pytest.mark.parametrize("auth", [None, aiohttp.BasicAuth("vcrpy", "test")])
def test_headers(tmpdir, scheme, auth):
url = scheme + "://httpbin.org"
def test_headers(tmpdir, auth, mockbin_request_url):
url = mockbin_request_url
with vcr.use_cassette(str(tmpdir.join("headers.yaml"))):
response, _ = get(url, auth=auth)
@@ -67,8 +65,10 @@ def test_headers(tmpdir, scheme, auth):
assert "yarl.URL" not in cassette.data[0]
def test_case_insensitive_headers(tmpdir, scheme):
url = scheme + "://httpbin.org"
@pytest.mark.online
def test_case_insensitive_headers(tmpdir, mockbin_request_url):
url = mockbin_request_url
with vcr.use_cassette(str(tmpdir.join("whatever.yaml"))):
_, _ = get(url)
@@ -79,8 +79,10 @@ def test_case_insensitive_headers(tmpdir, scheme):
assert cassette.play_count == 1
def test_text(tmpdir, scheme):
url = scheme + "://httpbin.org"
@pytest.mark.online
def test_text(tmpdir, mockbin_request_url):
url = mockbin_request_url
with vcr.use_cassette(str(tmpdir.join("text.yaml"))):
_, response_text = get(url)
@@ -90,8 +92,9 @@ def test_text(tmpdir, scheme):
assert cassette.play_count == 1
def test_json(tmpdir, scheme):
url = scheme + "://httpbin.org/get"
@pytest.mark.online
def test_json(tmpdir, mockbin_request_url):
url = mockbin_request_url
headers = {"Content-Type": "application/json"}
with vcr.use_cassette(str(tmpdir.join("json.yaml"))):
@@ -103,8 +106,9 @@ def test_json(tmpdir, scheme):
assert cassette.play_count == 1
def test_binary(tmpdir, scheme):
url = scheme + "://httpbin.org/image/png"
@pytest.mark.online
def test_binary(tmpdir, mockbin_request_url):
url = mockbin_request_url + "/image/png"
with vcr.use_cassette(str(tmpdir.join("binary.yaml"))):
_, response_binary = get(url, output="raw")
@@ -114,23 +118,25 @@ def test_binary(tmpdir, scheme):
assert cassette.play_count == 1
def test_stream(tmpdir, scheme):
url = scheme + "://httpbin.org/get"
@pytest.mark.online
def test_stream(tmpdir, mockbin_request_url):
url = mockbin_request_url
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:
cassette_resp, cassette_body = get(url, output="stream")
_, cassette_body = get(url, output="stream")
assert cassette_body == body
assert cassette.play_count == 1
@pytest.mark.online
@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)
data = {"key1": "value1", "key2": "value2"}
url = scheme + "://httpbin.org/post"
url = mockbin_request_url
with vcr.use_cassette(str(tmpdir.join("post.yaml"))):
_, response_json = post(url, **{body: data})
@@ -151,14 +157,15 @@ def test_post(tmpdir, scheme, body, caplog):
), "Log message not found."
def test_params(tmpdir, scheme):
url = scheme + "://httpbin.org/get?d=d"
@pytest.mark.online
def test_params(tmpdir, mockbin_request_url):
url = mockbin_request_url + "?d=d"
headers = {"Content-Type": "application/json"}
params = {"a": 1, "b": 2, "c": "c"}
with vcr.use_cassette(str(tmpdir.join("get.yaml"))) as cassette:
_, response_json = get(url, output="json", params=params, headers=headers)
assert response_json["args"] == {"a": "1", "b": "2", "c": "c", "d": "d"}
assert response_json["queryString"] == {"a": "1", "b": "2", "c": "c", "d": "d"}
with vcr.use_cassette(str(tmpdir.join("get.yaml"))) as cassette:
_, cassette_response_json = get(url, output="json", params=params, headers=headers)
@@ -166,8 +173,9 @@ def test_params(tmpdir, scheme):
assert cassette.play_count == 1
def test_params_same_url_distinct_params(tmpdir, scheme):
url = scheme + "://httpbin.org/get"
@pytest.mark.online
def test_params_same_url_distinct_params(tmpdir, mockbin_request_url):
url = mockbin_request_url
headers = {"Content-Type": "application/json"}
params = {"a": 1, "b": 2, "c": "c"}
@@ -185,8 +193,9 @@ def test_params_same_url_distinct_params(tmpdir, scheme):
get(url, output="text", params=other_params)
def test_params_on_url(tmpdir, scheme):
url = scheme + "://httpbin.org/get?a=1&b=foo"
@pytest.mark.online
def test_params_on_url(tmpdir, mockbin_request_url):
url = mockbin_request_url + "?a=1&b=foo"
headers = {"Content-Type": "application/json"}
with vcr.use_cassette(str(tmpdir.join("get.yaml"))) as cassette:
@@ -250,8 +259,9 @@ def test_aiohttp_test_client_json(aiohttp_client, tmpdir):
assert cassette.play_count == 1
def test_redirect(aiohttp_client, tmpdir):
url = "https://mockbin.org/redirect/302/2"
@pytest.mark.online
def test_redirect(tmpdir, mockbin):
url = mockbin + "/redirect/302/2"
with vcr.use_cassette(str(tmpdir.join("redirect.yaml"))):
response, _ = get(url)
@@ -274,9 +284,10 @@ def test_redirect(aiohttp_client, tmpdir):
assert cassette_response.request_info.real_url == response.request_info.real_url
def test_not_modified(aiohttp_client, tmpdir):
@pytest.mark.online
def test_not_modified(tmpdir, mockbin):
"""It doesn't try to redirect on 304"""
url = "https://httpbin.org/status/304"
url = mockbin + "/status/304"
with vcr.use_cassette(str(tmpdir.join("not_modified.yaml"))):
response, _ = get(url)
@@ -291,13 +302,14 @@ def test_not_modified(aiohttp_client, tmpdir):
assert cassette.play_count == 1
def test_double_requests(tmpdir):
@pytest.mark.online
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"))):
_, response_text1 = get(url, output="text")
@@ -322,31 +334,35 @@ def test_double_requests(tmpdir):
assert cassette.play_count == 2
def test_cookies(scheme, tmpdir):
def test_cookies(httpbin_both, httpbin_ssl_context, tmpdir):
async def run(loop):
cookies_url = scheme + (
"://httpbin.org/response-headers?"
cookies_url = httpbin_both.url + (
"/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/"
home_url = httpbin_both.url + "/"
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)
async with aiohttp.ClientSession(loop=loop, cookie_jar=aiohttp.CookieJar(unsafe=True)) as session:
cookies_resp = await session.get(cookies_url, ssl=httpbin_ssl_context)
home_resp = await session.get(
home_url, cookies=req_cookies, headers=req_headers, ssl=httpbin_ssl_context
)
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)
async with aiohttp.ClientSession(loop=loop, cookie_jar=aiohttp.CookieJar(unsafe=True)) as session:
cookies_resp = await session.get(cookies_url, ssl=httpbin_ssl_context)
home_resp = await session.get(
home_url, cookies=req_cookies, headers=req_headers, ssl=httpbin_ssl_context
)
assert cassette.play_count == 2
assert_responses(cookies_resp, home_resp)
@@ -362,16 +378,16 @@ def test_cookies(scheme, tmpdir):
run_in_loop(run)
def test_cookies_redirect(scheme, tmpdir):
def test_cookies_redirect(httpbin_both, httpbin_ssl_context, 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"
cookies_url = httpbin_both.url + "/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)
async with aiohttp.ClientSession(loop=loop, cookie_jar=aiohttp.CookieJar(unsafe=True)) as session:
cookies_resp = await session.get(cookies_url, ssl=httpbin_ssl_context)
assert not cookies_resp.cookies
cookies = session.cookie_jar.filter_cookies(cookies_url)
assert cookies["Cookie_1"].value == "Val_1"
@@ -380,8 +396,8 @@ def test_cookies_redirect(scheme, tmpdir):
# -------------------------- 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)
async with aiohttp.ClientSession(loop=loop, cookie_jar=aiohttp.CookieJar(unsafe=True)) as session:
cookies_resp = await session.get(cookies_url, ssl=httpbin_ssl_context)
assert not cookies_resp.cookies
cookies = session.cookie_jar.filter_cookies(cookies_url)
assert cookies["Cookie_1"].value == "Val_1"
@@ -393,8 +409,8 @@ def test_cookies_redirect(scheme, tmpdir):
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)
async with aiohttp.ClientSession(loop=loop, cookie_jar=aiohttp.CookieJar(unsafe=True)) as session:
cookies_resp = await session.get(cookies_url, ssl=httpbin_ssl_context)
assert not cookies_resp.cookies
cookies = session.cookie_jar.filter_cookies(cookies_url)
assert cookies["Cookie_1"].value == "Val_1"
@@ -402,8 +418,9 @@ def test_cookies_redirect(scheme, tmpdir):
run_in_loop(run)
def test_not_allow_redirects(tmpdir):
url = "https://mockbin.org/redirect/308/5"
@pytest.mark.online
def test_not_allow_redirects(tmpdir, mockbin):
url = mockbin + "/redirect/308/5"
path = str(tmpdir.join("redirects.yaml"))
with vcr.use_cassette(path):

View File

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

View File

@@ -1,10 +1,12 @@
import pytest
import os
import pytest
boto3 = pytest.importorskip("boto3")
import boto3 # NOQA
import botocore # NOQA
import vcr # NOQA
try:
@@ -55,24 +57,6 @@ def get_user(iam_client):
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(
os.environ.get("TRAVIS_PULL_REQUEST") != "false",
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",
)
def test_boto_medium_difficulty(tmpdir, get_user):
with vcr.use_cassette(str(tmpdir.join("boto3-medium.yml"))):
response = get_user()
assert response["User"]["UserName"] == IAM_USER_NAME

View File

@@ -1,16 +1,19 @@
import os
import json
import pytest
import vcr
import os
from urllib.request import urlopen
import pytest
def test_set_serializer_default_config(tmpdir, httpbin):
import vcr
@pytest.mark.online
def test_set_serializer_default_config(tmpdir, mockbin_request_url):
my_vcr = vcr.VCR(serializer="json")
with my_vcr.use_cassette(str(tmpdir.join("test.json"))):
assert my_vcr.serializer == "json"
urlopen(httpbin.url + "/get")
urlopen(mockbin_request_url)
with open(str(tmpdir.join("test.json"))) as f:
file_content = f.read()
@@ -18,35 +21,38 @@ def test_set_serializer_default_config(tmpdir, httpbin):
assert json.loads(file_content)
def test_default_set_cassette_library_dir(tmpdir, httpbin):
@pytest.mark.online
def test_default_set_cassette_library_dir(tmpdir, mockbin_request_url):
my_vcr = vcr.VCR(cassette_library_dir=str(tmpdir.join("subdir")))
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")))
def test_override_set_cassette_library_dir(tmpdir, httpbin):
@pytest.mark.online
def test_override_set_cassette_library_dir(tmpdir, mockbin_request_url):
my_vcr = vcr.VCR(cassette_library_dir=str(tmpdir.join("subdir")))
cld = str(tmpdir.join("subdir2"))
with my_vcr.use_cassette("test.json", cassette_library_dir=cld):
urlopen(httpbin.url + "/get")
urlopen(mockbin_request_url)
assert os.path.exists(str(tmpdir.join("subdir2").join("test.json")))
assert not os.path.exists(str(tmpdir.join("subdir").join("test.json")))
def test_override_match_on(tmpdir, httpbin):
@pytest.mark.online
def test_override_match_on(tmpdir, mockbin_request_url):
my_vcr = vcr.VCR(match_on=["method"])
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:
urlopen(httpbin.url + "/get")
urlopen(mockbin_request_url)
assert len(cass) == 1
assert cass.play_count == 1
@@ -58,3 +64,24 @@ def test_missing_matcher():
with pytest.raises(KeyError):
with my_vcr.use_cassette("test.yaml", match_on=["notawesome"]):
pass
@pytest.mark.online
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

@@ -6,23 +6,26 @@ import os
import time
from urllib.request import urlopen
import pytest
# Internal imports
import vcr
def test_disk_saver_nowrite(tmpdir, httpbin):
@pytest.mark.online
def test_disk_saver_nowrite(tmpdir, mockbin_request_url):
"""
Ensure that when you close a cassette without changing it it doesn't
rewrite the file
"""
fname = str(tmpdir.join("synopsis.yaml"))
with vcr.use_cassette(fname) as cass:
urlopen(httpbin.url).read()
urlopen(mockbin_request_url).read()
assert cass.play_count == 0
last_mod = os.path.getmtime(fname)
with vcr.use_cassette(fname) as cass:
urlopen(httpbin.url).read()
urlopen(mockbin_request_url).read()
assert cass.play_count == 1
assert cass.dirty is False
last_mod2 = os.path.getmtime(fname)
@@ -30,14 +33,15 @@ def test_disk_saver_nowrite(tmpdir, httpbin):
assert last_mod == last_mod2
def test_disk_saver_write(tmpdir, httpbin):
@pytest.mark.online
def test_disk_saver_write(tmpdir, mockbin_request_url):
"""
Ensure that when you close a cassette after changing it it does
rewrite the file
"""
fname = str(tmpdir.join("synopsis.yaml"))
with vcr.use_cassette(fname) as cass:
urlopen(httpbin.url).read()
urlopen(mockbin_request_url).read()
assert cass.play_count == 0
last_mod = os.path.getmtime(fname)
@@ -46,8 +50,8 @@ def test_disk_saver_write(tmpdir, httpbin):
time.sleep(1)
with vcr.use_cassette(fname, record_mode=vcr.mode.ANY) as cass:
urlopen(httpbin.url).read()
urlopen(httpbin.url + "/get").read()
urlopen(mockbin_request_url).read()
urlopen(mockbin_request_url + "/get").read()
assert cass.play_count == 1
assert cass.dirty
last_mod2 = os.path.getmtime(fname)

View File

@@ -1,12 +1,14 @@
import base64
import pytest
from urllib.request import urlopen, Request
from urllib.parse import urlencode
from urllib.error import HTTPError
import vcr
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
import vcr
def _request_with_auth(url, username, password):
request = Request(url)
@@ -106,6 +108,18 @@ def test_decompress_gzip(tmpdir, httpbin):
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):
url = httpbin.url + "/deflate"
request = Request(url, headers={"Accept-Encoding": ["gzip, deflate"]})
@@ -128,3 +142,24 @@ def test_decompress_regular(tmpdir, httpbin):
resp = urlopen(url).read()
assert_cassette_has_one_response(cass)
assert_is_json(resp)
def test_before_record_request_corruption(tmpdir, httpbin):
"""Modifying request in before_record_request should not affect outgoing request"""
def before_record(request):
request.headers.clear()
request.body = b""
return request
req = Request(
httpbin.url + "/post",
data=urlencode({"test": "exists"}).encode(),
headers={"X-Test": "exists"},
)
cass_file = str(tmpdir.join("modified_response.yaml"))
with vcr.use_cassette(cass_file, before_record_request=before_record):
resp = json.loads(urlopen(req).read())
assert resp["headers"]["X-Test"] == "exists"
assert resp["form"]["test"] == "exists"

View File

@@ -1,16 +1,13 @@
# -*- coding: utf-8 -*-
"""Integration tests with httplib2"""
import sys
from urllib.parse import urlencode
import pytest
import pytest_httpbin.certs
from assertions import assert_cassette_has_one_response
import vcr
from assertions import assert_cassette_has_one_response
httplib2 = pytest.importorskip("httplib2")
@@ -20,8 +17,6 @@ def http():
with the certificate replaced by the httpbin one.
"""
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)
@@ -61,6 +56,7 @@ def test_response_headers(tmpdir, httpbin_both):
assert set(headers) == set(resp.items())
@pytest.mark.online
def test_effective_url(tmpdir):
"""Ensure that the effective_url is captured"""
url = "http://mockbin.org/redirect/301"

View File

@@ -1,6 +1,7 @@
import pytest
import os
import pytest
asyncio = pytest.importorskip("asyncio")
httpx = pytest.importorskip("httpx")
@@ -79,8 +80,6 @@ class DoAsyncRequest(BaseDoRequest):
def pytest_generate_tests(metafunc):
if "do_request" in metafunc.fixturenames:
metafunc.parametrize("do_request", [DoAsyncRequest, DoSyncRequest])
if "scheme" in metafunc.fixturenames:
metafunc.parametrize("scheme", ["http", "https"])
@pytest.fixture
@@ -88,8 +87,10 @@ def yml(tmpdir, request):
return str(tmpdir.join(request.function.__name__ + ".yaml"))
def test_status(tmpdir, scheme, do_request):
url = scheme + "://mockbin.org/request"
@pytest.mark.online
def test_status(tmpdir, mockbin, do_request):
url = mockbin
with vcr.use_cassette(str(tmpdir.join("status.yaml"))):
response = do_request()("GET", url)
@@ -99,8 +100,10 @@ def test_status(tmpdir, scheme, do_request):
assert cassette.play_count == 1
def test_case_insensitive_headers(tmpdir, scheme, do_request):
url = scheme + "://mockbin.org/request"
@pytest.mark.online
def test_case_insensitive_headers(tmpdir, mockbin, do_request):
url = mockbin
with vcr.use_cassette(str(tmpdir.join("whatever.yaml"))):
do_request()("GET", url)
@@ -111,8 +114,10 @@ def test_case_insensitive_headers(tmpdir, scheme, do_request):
assert cassette.play_count == 1
def test_content(tmpdir, scheme, do_request):
url = scheme + "://httpbin.org"
@pytest.mark.online
def test_content(tmpdir, mockbin, do_request):
url = mockbin
with vcr.use_cassette(str(tmpdir.join("cointent.yaml"))):
response = do_request()("GET", url)
@@ -122,9 +127,11 @@ def test_content(tmpdir, scheme, do_request):
assert cassette.play_count == 1
def test_json(tmpdir, scheme, do_request):
url = scheme + "://httpbin.org/get"
headers = {"Content-Type": "application/json"}
@pytest.mark.online
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)
@@ -135,8 +142,9 @@ def test_json(tmpdir, scheme, do_request):
assert cassette.play_count == 1
def test_params_same_url_distinct_params(tmpdir, scheme, do_request):
url = scheme + "://httpbin.org/get"
@pytest.mark.online
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"}
@@ -155,8 +163,9 @@ def test_params_same_url_distinct_params(tmpdir, scheme, do_request):
do_request()("GET", url, params=params, headers=headers)
def test_redirect(tmpdir, do_request, yml):
url = "https://mockbin.org/redirect/303/2"
@pytest.mark.online
def test_redirect(mockbin, yml, do_request):
url = mockbin + "/redirect/303/2"
redirect_kwargs = {HTTPX_REDIRECT_PARAM.name: True}
@@ -181,20 +190,25 @@ def test_redirect(tmpdir, do_request, yml):
}
def test_work_with_gzipped_data(tmpdir, do_request, yml):
@pytest.mark.online
def test_work_with_gzipped_data(mockbin, do_request, yml):
url = mockbin + "/gzip?foo=bar"
headers = {"accept-encoding": "deflate, gzip"}
with vcr.use_cassette(yml):
do_request()("GET", "https://httpbin.org/gzip")
do_request(headers=headers)("GET", url)
with vcr.use_cassette(yml) as cassette:
cassette_response = do_request()("GET", "https://httpbin.org/gzip")
cassette_response = do_request(headers=headers)("GET", url)
assert "gzip" in cassette_response.json()["headers"]["Accept-Encoding"]
assert cassette_response.headers["content-encoding"] == "gzip"
assert cassette_response.read()
assert cassette.play_count == 1
@pytest.mark.online
@pytest.mark.parametrize("url", ["https://github.com/kevin1024/vcrpy/issues/" + str(i) for i in range(3, 6)])
def test_simple_fetching(tmpdir, do_request, yml, url):
def test_simple_fetching(do_request, yml, url):
with vcr.use_cassette(yml):
do_request()("GET", url)
@@ -209,7 +223,7 @@ def test_behind_proxy(do_request):
yml = (
os.path.dirname(os.path.realpath(__file__)) + "/cassettes/" + "test_httpx_test_test_behind_proxy.yml"
)
url = "https://httpbin.org/headers"
url = "https://mockbin.org/headers"
proxy = "http://localhost:8080"
proxies = {"http://": proxy, "https://": proxy}
@@ -225,46 +239,49 @@ def test_behind_proxy(do_request):
assert cassette_response.request.url == response.request.url
def test_cookies(tmpdir, scheme, do_request):
@pytest.mark.online
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]
with do_request() as client:
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}
url = scheme + "://httpbin.org"
testfile = str(tmpdir.join("cookies.yml"))
with vcr.use_cassette(testfile):
r1 = client("GET", url + "/cookies/set?k1=v1&k2=v2", **redirect_kwargs)
assert response_cookies(r1.history[0]) == ["k1", "k2"]
assert response_cookies(r1) == []
r1 = client("GET", url, **redirect_kwargs)
r2 = client("GET", url + "/cookies", **redirect_kwargs)
assert len(r2.json()["cookies"]) == 2
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() as new_client:
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 + "/cookies/set?k1=v1&k2=v2")
assert response_cookies(cassette_response.history[0]) == ["k1", "k2"]
assert response_cookies(cassette_response) == []
cassette_response = new_client("GET", url)
assert cassette.play_count == 2
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):
@pytest.mark.online
def test_relative_redirects(tmpdir, scheme, do_request, mockbin):
redirect_kwargs = {HTTPX_REDIRECT_PARAM.name: True}
url = scheme + "://mockbin.com/redirect/301?to=/redirect/301?to=/request"
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)
@@ -279,8 +296,9 @@ def test_relative_redirects(tmpdir, scheme, do_request):
assert cassette.play_count == 3
def test_redirect_wo_allow_redirects(do_request, yml):
url = "https://mockbin.org/redirect/308/5"
@pytest.mark.online
def test_redirect_wo_allow_redirects(do_request, mockbin, yml):
url = mockbin + "/redirect/308/5"
redirect_kwargs = {HTTPX_REDIRECT_PARAM.name: False}

View File

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

View File

@@ -1,7 +1,8 @@
import vcr
import pytest
from urllib.request import urlopen
import pytest
import vcr
DEFAULT_URI = "http://httpbin.org/get?p1=q1&p2=q2" # base uri for testing
@@ -35,7 +36,6 @@ def cassette(tmpdir, httpbin, httpbin_secure):
],
)
def test_matchers(httpbin, httpbin_secure, cassette, matcher, matching_uri, not_matching_uri):
matching_uri = _replace_httpbin(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)
@@ -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"]
)
def test_default_matcher_matches(cassette, uri, httpbin, httpbin_secure):
uri = _replace_httpbin(uri, httpbin, httpbin_secure)
with vcr.use_cassette(cassette) as cass:

View File

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

View File

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

View File

@@ -1,7 +1,9 @@
import pytest
import vcr
from urllib.request import urlopen
import pytest
import vcr
def test_once_record_mode(tmpdir, httpbin):
testfile = str(tmpdir.join("recordmode.yml"))

View File

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

View File

@@ -10,7 +10,7 @@ import vcr
from vcr.persisters.filesystem import FilesystemPersister
class CustomFilesystemPersister(object):
class CustomFilesystemPersister:
"""Behaves just like default FilesystemPersister but adds .test extension
to the cassette file"""

View File

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

View File

@@ -1,8 +1,9 @@
"""Test requests' interaction with vcr"""
import pytest
import vcr
from assertions import assert_cassette_empty, assert_is_json
import vcr
requests = pytest.importorskip("requests")
from requests.exceptions import ConnectionError # noqa E402
@@ -143,6 +144,17 @@ def test_redirects(tmpdir, httpbin_both):
assert cass.play_count == 2
def test_raw_stream(tmpdir, httpbin):
expected_response = requests.get(httpbin.url, stream=True)
expected_content = b"".join(expected_response.raw.stream())
for _ in range(2): # one for recording, one for cassette reply
with vcr.use_cassette(str(tmpdir.join("raw_stream.yaml"))):
actual_response = requests.get(httpbin.url, stream=True)
actual_content = b"".join(actual_response.raw.stream())
assert actual_content == expected_content
def test_cross_scheme(tmpdir, httpbin_secure, httpbin):
"""Ensure that requests between schemes are treated separately"""
# First fetch a url under http, and then again under https and then
@@ -155,20 +167,40 @@ def test_cross_scheme(tmpdir, httpbin_secure, httpbin):
assert len(cass) == 2
def test_gzip(tmpdir, httpbin_both):
def test_gzip__decode_compressed_response_false(tmpdir, httpbin_both):
"""
Ensure that requests (actually urllib3) is able to automatically decompress
the response body
"""
for _ in range(2): # one for recording, one for re-playing
with vcr.use_cassette(str(tmpdir.join("gzip.yaml"))):
response = requests.get(httpbin_both + "/gzip")
assert response.headers["content-encoding"] == "gzip" # i.e. not removed
assert_is_json(response.content) # i.e. uncompressed bytes
def test_gzip__decode_compressed_response_true(tmpdir, httpbin_both):
url = httpbin_both + "/gzip"
response = requests.get(url)
with vcr.use_cassette(str(tmpdir.join("gzip.yaml"))):
response = requests.get(url)
assert_is_json(response.content)
expected_response = requests.get(url)
expected_content = expected_response.content
assert expected_response.headers["content-encoding"] == "gzip" # self-test
with vcr.use_cassette(str(tmpdir.join("gzip.yaml"))):
assert_is_json(response.content)
with vcr.use_cassette(
str(tmpdir.join("decode_compressed.yaml")), decode_compressed_response=True
) as cassette:
r = requests.get(url)
assert r.headers["content-encoding"] == "gzip" # i.e. not removed
assert r.content == expected_content
# Has the cassette body been decompressed?
cassette_response_body = cassette.responses[0]["body"]["string"]
assert isinstance(cassette_response_body, str)
with vcr.use_cassette(str(tmpdir.join("decode_compressed.yaml")), decode_compressed_response=True):
r = requests.get(url)
assert "content-encoding" not in r.headers # i.e. removed
assert r.content == expected_content
def test_session_and_connection_close(tmpdir, httpbin):

View File

@@ -1,10 +1,11 @@
import vcr
import zlib
import json
import http.client as httplib
import json
import zlib
from assertions import assert_is_json
import vcr
def _headers_are_case_insensitive(host, port):
conn = httplib.HTTPConnection(host, port)

View File

@@ -4,11 +4,11 @@
import json
import pytest
from assertions import assert_cassette_empty, assert_is_json
import vcr
from vcr.errors import CannotOverwriteExistingCassetteException
from assertions import assert_cassette_empty, assert_is_json
tornado = pytest.importorskip("tornado")
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))
@pytest.fixture(params=["https", "http"])
def scheme(request):
"""Fixture that returns both http and https."""
return request.param
@pytest.mark.gen_test
def test_status_code(get_client, scheme, tmpdir):
"""Ensure that we can read the status code"""

View File

@@ -2,15 +2,16 @@
"""Integration tests with urllib2"""
import ssl
from urllib.request import urlopen
from urllib.parse import urlencode
from urllib.request import urlopen
import pytest_httpbin.certs
from assertions import assert_cassette_has_one_response
from pytest import mark
# Internal imports
import vcr
from assertions import assert_cassette_has_one_response
def urlopen_with_cafile(*args, **kwargs):
context = ssl.create_default_context(cafile=pytest_httpbin.certs.where())
@@ -56,6 +57,7 @@ def test_response_headers(httpbin_both, tmpdir):
assert sorted(open1) == sorted(open2)
@mark.online
def test_effective_url(tmpdir):
"""Ensure that the effective_url is captured"""
url = "http://mockbin.org/redirect/301"

View File

@@ -4,9 +4,11 @@
import pytest
import pytest_httpbin
from assertions import assert_cassette_empty, assert_is_json
import vcr
from vcr.patch import force_reset
from assertions import assert_cassette_empty, assert_is_json
from vcr.stubs.compat import get_headers
urllib3 = pytest.importorskip("urllib3")
@@ -40,7 +42,8 @@ def test_headers(tmpdir, httpbin_both, verify_pool_mgr):
headers = verify_pool_mgr.request("GET", url).headers
with vcr.use_cassette(str(tmpdir.join("headers.yaml"))):
assert headers == verify_pool_mgr.request("GET", url).headers
new_headers = verify_pool_mgr.request("GET", url).headers
assert sorted(get_headers(headers)) == sorted(get_headers(new_headers))
def test_body(tmpdir, httpbin_both, verify_pool_mgr):
@@ -94,6 +97,7 @@ def test_post(tmpdir, httpbin_both, verify_pool_mgr):
assert req1 == req2
@pytest.mark.online
def test_redirects(tmpdir, verify_pool_mgr):
"""Ensure that we can handle redirects"""
url = "http://mockbin.org/redirect/301"
@@ -144,18 +148,18 @@ def test_https_with_cert_validation_disabled(tmpdir, httpbin_secure, pool_mgr):
def test_urllib3_force_reset():
cpool = urllib3.connectionpool
http_original = cpool.HTTPConnection
https_original = cpool.HTTPSConnection
verified_https_original = cpool.VerifiedHTTPSConnection
conn = urllib3.connection
http_original = conn.HTTPConnection
https_original = conn.HTTPSConnection
verified_https_original = conn.VerifiedHTTPSConnection
with vcr.use_cassette(path="test"):
first_cassette_HTTPConnection = cpool.HTTPConnection
first_cassette_HTTPSConnection = cpool.HTTPSConnection
first_cassette_VerifiedHTTPSConnection = cpool.VerifiedHTTPSConnection
first_cassette_HTTPConnection = conn.HTTPConnection
first_cassette_HTTPSConnection = conn.HTTPSConnection
first_cassette_VerifiedHTTPSConnection = conn.VerifiedHTTPSConnection
with force_reset():
assert cpool.HTTPConnection is http_original
assert cpool.HTTPSConnection is https_original
assert cpool.VerifiedHTTPSConnection is verified_https_original
assert cpool.HTTPConnection is first_cassette_HTTPConnection
assert cpool.HTTPSConnection is first_cassette_HTTPSConnection
assert cpool.VerifiedHTTPSConnection is first_cassette_VerifiedHTTPSConnection
assert conn.HTTPConnection is http_original
assert conn.HTTPSConnection is https_original
assert conn.VerifiedHTTPSConnection is verified_https_original
assert conn.HTTPConnection is first_cassette_HTTPConnection
assert conn.HTTPSConnection is first_cassette_HTTPSConnection
assert conn.VerifiedHTTPSConnection is first_cassette_VerifiedHTTPSConnection

View File

@@ -1,9 +1,10 @@
import http.client as httplib
import multiprocessing
import pytest
from xmlrpc.client import ServerProxy
from xmlrpc.server import SimpleXMLRPCServer
import pytest
requests = pytest.importorskip("requests")
import vcr # NOQA
@@ -51,6 +52,7 @@ def test_flickr_multipart_upload(httpbin, tmpdir):
assert cass.play_count == 1
@pytest.mark.online
def test_flickr_should_respond_with_200(tmpdir):
testfile = str(tmpdir.join("flickr.yml"))
with vcr.use_cassette(testfile):
@@ -63,11 +65,13 @@ def test_cookies(tmpdir, httpbin):
with vcr.use_cassette(testfile):
s = requests.Session()
s.get(httpbin.url + "/cookies/set?k1=v1&k2=v2")
assert s.cookies.keys() == ["k1", "k2"]
r2 = s.get(httpbin.url + "/cookies")
assert len(r2.json()["cookies"]) == 2
assert sorted(r2.json()["cookies"].keys()) == ["k1", "k2"]
@pytest.mark.online
def test_amazon_doctype(tmpdir):
# amazon gzips its homepage. For some reason, in requests 2.7, it's not
# getting gunzipped.
@@ -83,7 +87,7 @@ def start_rpc_server(q):
httpd.serve_forever()
@pytest.yield_fixture(scope="session")
@pytest.fixture(scope="session")
def rpc_server():
q = multiprocessing.Queue()
proxy_process = multiprocessing.Process(target=start_rpc_server, args=(q,))

View File

@@ -7,6 +7,7 @@ from unittest import mock
import pytest
import yaml
from vcr.cassette import Cassette
from vcr.errors import UnhandledHTTPRequestError
from vcr.patch import force_reset

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 json
from unittest import mock
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():
@@ -297,6 +298,18 @@ def test_decode_response_deflate():
assert decoded_response["headers"]["content-length"] == [str(len(body))]
def test_decode_response_deflate_already_decompressed():
body = b"deflate message"
gzip_response = {
"body": {"string": body},
"headers": {
"content-encoding": ["deflate"],
},
}
decoded_response = decode_response(gzip_response)
assert decoded_response["body"]["string"] == body
def test_decode_response_gzip():
body = b"gzip message"
@@ -324,3 +337,15 @@ def test_decode_response_gzip():
decoded_response = decode_response(gzip_response)
assert decoded_response["body"]["string"] == body
assert decoded_response["headers"]["content-length"] == [str(len(body))]
def test_decode_response_gzip_already_decompressed():
body = b"gzip message"
gzip_response = {
"body": {"string": body},
"headers": {
"content-encoding": ["gzip"],
},
}
decoded_response = decode_response(gzip_response)
assert decoded_response["body"]["string"] == body

View File

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

View File

@@ -3,8 +3,7 @@ from unittest import mock
import pytest
from vcr import matchers
from vcr import request
from vcr import matchers, request
# the dict contains requests with corresponding to its key difference
# with 'base' request.

View File

@@ -1,6 +1,7 @@
import filecmp
import json
import shutil
import yaml
import vcr.migration

View File

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

View File

@@ -93,7 +93,7 @@ def test_response_parses_correctly_and_fp_attribute_error_is_not_thrown():
},
}
vcr_response = VCRHTTPResponse(recorded_response)
handle = io.TextIOWrapper(io.BufferedReader(vcr_response), encoding="utf-8")
handle = io.TextIOWrapper(vcr_response, encoding="utf-8")
handle = iter(handle)
articles = [line for line in handle]
assert len(articles) > 1

View File

@@ -5,7 +5,7 @@ import pytest
from vcr.request import Request
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():

View File

@@ -1,8 +1,10 @@
from unittest import mock
from pytest import mark
from vcr import mode
from vcr.stubs import VCRHTTPSConnection
from vcr.cassette import Cassette
from vcr.stubs import VCRHTTPSConnection
class TestVCRConnection:
@@ -11,6 +13,7 @@ class TestVCRConnection:
vcr_connection.ssl_version = "example_ssl_version"
assert vcr_connection.real_connection.ssl_version == "example_ssl_version"
@mark.online
@mock.patch("vcr.cassette.Cassette.can_play_response_for", return_value=False)
def testing_connect(*args):
vcr_connection = VCRHTTPSConnection("www.google.com")

199
tests/unit/test_unittest.py Normal file
View File

@@ -0,0 +1,199 @@
import os
from unittest import TextTestRunner, defaultTestLoader
from unittest.mock import MagicMock
from urllib.request import urlopen
import pytest
from vcr.unittest import VCRTestCase
def test_defaults():
class MyTest(VCRTestCase):
def test_foo(self):
pass
test = run_testcase(MyTest)[0][0]
expected_path = os.path.join(os.path.dirname(__file__), "cassettes")
expected_name = "MyTest.test_foo.yaml"
assert os.path.dirname(test.cassette._path) == expected_path
assert os.path.basename(test.cassette._path) == expected_name
def test_disabled():
# Baseline vcr_enabled = True
class MyTest(VCRTestCase):
def test_foo(self):
pass
test = run_testcase(MyTest)[0][0]
assert hasattr(test, "cassette")
# Test vcr_enabled = False
class MyTest(VCRTestCase):
vcr_enabled = False
def test_foo(self):
pass
test = run_testcase(MyTest)[0][0]
assert not hasattr(test, "cassette")
def test_cassette_library_dir():
class MyTest(VCRTestCase):
def test_foo(self):
pass
def _get_cassette_library_dir(self):
return "/testing"
test = run_testcase(MyTest)[0][0]
assert test.cassette._path.startswith("/testing/")
def test_cassette_name():
class MyTest(VCRTestCase):
def test_foo(self):
pass
def _get_cassette_name(self):
return "my-custom-name"
test = run_testcase(MyTest)[0][0]
assert os.path.basename(test.cassette._path) == "my-custom-name"
def test_vcr_kwargs_overridden():
class MyTest(VCRTestCase):
def test_foo(self):
pass
def _get_vcr_kwargs(self):
kwargs = super()._get_vcr_kwargs()
kwargs["record_mode"] = "new_episodes"
return kwargs
test = run_testcase(MyTest)[0][0]
assert test.cassette.record_mode == "new_episodes"
def test_vcr_kwargs_passed():
class MyTest(VCRTestCase):
def test_foo(self):
pass
def _get_vcr_kwargs(self):
return super()._get_vcr_kwargs(
record_mode="new_episodes",
)
test = run_testcase(MyTest)[0][0]
assert test.cassette.record_mode == "new_episodes"
def test_vcr_kwargs_cassette_dir():
# Test that _get_cassette_library_dir applies if cassette_library_dir
# is absent from vcr kwargs.
class MyTest(VCRTestCase):
def test_foo(self):
pass
def _get_vcr_kwargs(self):
return dict(
record_mode="new_episodes",
)
_get_cassette_library_dir = MagicMock(return_value="/testing")
test = run_testcase(MyTest)[0][0]
assert test.cassette._path.startswith("/testing/")
assert test._get_cassette_library_dir.call_count == 1
# Test that _get_cassette_library_dir is ignored if cassette_library_dir
# is present in vcr kwargs.
class MyTest(VCRTestCase):
def test_foo(self):
pass
def _get_vcr_kwargs(self):
return dict(
cassette_library_dir="/testing",
)
_get_cassette_library_dir = MagicMock(return_value="/ignored")
test = run_testcase(MyTest)[0][0]
assert test.cassette._path.startswith("/testing/")
assert test._get_cassette_library_dir.call_count == 0
@pytest.mark.online
def test_get_vcr_with_matcher(tmpdir):
cassette_dir = tmpdir.mkdir("cassettes")
assert len(cassette_dir.listdir()) == 0
mock_matcher = MagicMock(return_value=True, __name__="MockMatcher")
class MyTest(VCRTestCase):
def test_foo(self):
self.response = urlopen("http://example.com").read()
def _get_vcr(self):
myvcr = super()._get_vcr()
myvcr.register_matcher("mymatcher", mock_matcher)
myvcr.match_on = ["mymatcher"]
return myvcr
def _get_cassette_library_dir(self):
return str(cassette_dir)
# First run to fill cassette.
test = run_testcase(MyTest)[0][0]
assert len(test.cassette.requests) == 1
assert not mock_matcher.called # nothing in cassette
# Second run to call matcher.
test = run_testcase(MyTest)[0][0]
assert len(test.cassette.requests) == 1
assert mock_matcher.called
assert (
repr(mock_matcher.mock_calls[0])
== "call(<Request (GET) http://example.com>, <Request (GET) http://example.com>)"
)
@pytest.mark.online
def test_testcase_playback(tmpdir):
cassette_dir = tmpdir.mkdir("cassettes")
assert len(cassette_dir.listdir()) == 0
# First test actually reads from the web.
class MyTest(VCRTestCase):
def test_foo(self):
self.response = urlopen("http://example.com").read()
def _get_cassette_library_dir(self):
return str(cassette_dir)
test = run_testcase(MyTest)[0][0]
assert b"illustrative examples" in test.response
assert len(test.cassette.requests) == 1
assert test.cassette.play_count == 0
# Second test reads from cassette.
test2 = run_testcase(MyTest)[0][0]
assert test.cassette is not test2.cassette
assert b"illustrative examples" in test.response
assert len(test2.cassette.requests) == 1
assert test2.cassette.play_count == 1
def run_testcase(testcase_class):
"""Run all the tests in a TestCase and return them."""
suite = defaultTestLoader.loadTestsFromTestCase(testcase_class)
tests = list(suite._tests)
result = TextTestRunner().run(suite)
return tests, result

View File

@@ -1,13 +1,14 @@
from unittest import mock
import http.client as httplib
import os
from pathlib import Path
from unittest import mock
import pytest
import http.client as httplib
from vcr import VCR, mode, use_cassette
from vcr.patch import _HTTPConnection, force_reset
from vcr.request import Request
from vcr.stubs import VCRHTTPSConnection
from vcr.patch import _HTTPConnection, force_reset
def test_vcr_use_cassette():
@@ -40,7 +41,7 @@ def test_vcr_use_cassette():
def test_vcr_before_record_request_params():
base_path = "http://httpbin.org/"
base_path = "http://whatever.test/"
def before_record_cb(request):
if request.path != "/get":
@@ -95,7 +96,6 @@ def test_vcr_before_record_response_iterable():
# Prevent actually saving the cassette
with mock.patch("vcr.cassette.FilesystemPersister.save_cassette"):
# Baseline: non-iterable before_record_response should work
mock_filter = mock.Mock()
vcr = VCR(before_record_response=mock_filter)
@@ -119,7 +119,6 @@ def test_before_record_response_as_filter():
# Prevent actually saving the cassette
with mock.patch("vcr.cassette.FilesystemPersister.save_cassette"):
filter_all = mock.Mock(return_value=None)
vcr = VCR(before_record_response=filter_all)
with vcr.use_cassette("test") as cassette:
@@ -133,7 +132,6 @@ def test_vcr_path_transformer():
# Prevent actually saving the cassette
with mock.patch("vcr.cassette.FilesystemPersister.save_cassette"):
# Baseline: path should be unchanged
vcr = VCR()
with vcr.use_cassette("test") as cassette:
@@ -360,3 +358,11 @@ def test_dynamically_added(self):
TestVCRClass.test_dynamically_added = 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):
if "vcr" in sys.modules:
# Remove imported module entry if already loaded in another test
del sys.modules["vcr"]

29
tox.ini
View File

@@ -3,18 +3,20 @@ skip_missing_interpreters=true
envlist =
cov-clean,
lint,
{py37,py38,py39,py310}-{requests,httplib2,urllib3,tornado4,boto3,aiohttp,httpx},
{pypy3}-{requests,httplib2,urllib3,tornado4,boto3},
{py37,py38,py39,py310,py311}-{requests-urllib3-1,httplib2,urllib3-1,tornado4,boto3,aiohttp,httpx},
{py310,py311}-{requests-urllib3-2,urllib3-2},
{pypy3}-{requests-urllib3-1,httplib2,urllib3-1,tornado4,boto3},
{py310}-httpx019,
cov-report
[gh-actions]
python =
3.7: py37, lint
3.7: py37
3.8: py38
3.9: py39
3.10: py310
3.10: py310, lint
3.11: py311
pypy-3: pypy3
# Coverage environment tasks: cov-clean and cov-report
@@ -36,13 +38,16 @@ skipsdist = True
commands =
black --version
black --check --diff .
isort --version
isort . --check --diff
flake8 --version
flake8 --exclude=./docs/conf.py,./.tox/
flake8 --exclude=./docs/conf.py,./.tox/,./venv/
pyflakes ./docs/conf.py
deps =
flake8
black
basepython = python3.7
isort
basepython = python3.10
[testenv:docs]
# Running sphinx from inside the "docs" directory
@@ -71,18 +76,20 @@ basepython = python3.7
usedevelop=true
commands =
./runtests.sh --cov=./vcr --cov-branch --cov-report=xml --cov-append {posargs}
allowlist_externals =
./runtests.sh
deps =
Werkzeug==2.0.3
pytest
git+https://github.com/immerrr/pytest-httpbin@fix-redirect-location-scheme-for-secure-server
pytest-httpbin>=1.0.1
pytest-cov
PyYAML
ipaddress
requests: requests>=2.22.0
httplib2: httplib2
urllib3: urllib3
urllib3-1: urllib3<2
urllib3-2: urllib3<3
boto3: boto3
boto3: urllib3
aiohttp: aiohttp
aiohttp: pytest-asyncio
aiohttp: pytest-aiohttp
@@ -94,8 +101,8 @@ deps =
httpx019: httpx==0.19
{py37,py38,py39,py310}-{httpx}: pytest-asyncio
depends =
lint,{py37,py38,py39,py310,pypy3}-{requests,httplib2,urllib3,tornado4,boto3},{py37,py38,py39,py310}-{aiohttp},{py37,py38,py39,py310}-{httpx}: cov-clean
cov-report: lint,{py37,py38,py39,py310,pypy3}-{requests,httplib2,urllib3,tornado4,boto3},{py37,py38,py39,py310}-{aiohttp}
lint,{py37,py38,py39,py310,py311,pypy3}-{requests-urllib3-1,httplib2,urllib3-1,tornado4,boto3},{py310,py311}-{requests-urllib3-2,urllib3-2},{py37,py38,py39,py310,py311}-{aiohttp},{py37,py38,py39,py310,py311}-{httpx}: cov-clean
cov-report: lint,{py37,py38,py39,py310,py311,pypy3}-{requests-urllib3-1,httplib2,urllib3-1,tornado4,boto3},{py310,py311}-{requests-urllib3-2,urllib3-2},{py37,py38,py39,py310,py311}-{aiohttp}
passenv =
AWS_ACCESS_KEY_ID
AWS_DEFAULT_REGION

View File

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

View File

@@ -1,20 +1,20 @@
import collections
import contextlib
import copy
import sys
import inspect
import logging
import sys
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 .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:
from asyncio import iscoroutinefunction
@@ -45,7 +45,11 @@ class CassetteContextDecorator:
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
def from_args(cls, cassette_class, **kwargs):
@@ -55,6 +59,7 @@ class CassetteContextDecorator:
self.cls = cls
self._args_getter = args_getter
self.__finish = None
self.__cassette = None
def _patch_generator(self, cassette):
with contextlib.ExitStack() as exit_stack:
@@ -64,9 +69,6 @@ class CassetteContextDecorator:
log.debug(log_format.format(action="Entering", path=cassette._path))
yield cassette
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):
# This assertion is here to prevent the dangerous behavior
@@ -84,10 +86,22 @@ class CassetteContextDecorator:
if other_kwargs.get("path_transformer"):
transformer = other_kwargs["path_transformer"]
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)
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)
self.__finish = None

View File

@@ -1,20 +1,19 @@
import copy
from collections import abc as collections_abc
import functools
import inspect
import os
import types
from collections import abc as collections_abc
from pathlib import Path
import six
from . import filters, matchers
from .cassette import Cassette
from .serializers import yamlserializer, jsonserializer
from .persisters.filesystem import FilesystemPersister
from .util import compose, auto_decorate
from .record_mode import RecordMode
from . import matchers
from . import filters
from .serializers import jsonserializer, yamlserializer
from .util import auto_decorate, compose
class VCR:
@@ -50,6 +49,7 @@ class VCR:
cassette_library_dir=None,
func_path_generator=None,
decode_compressed_response=False,
record_on_exception=True,
):
self.serializer = serializer
self.match_on = match_on
@@ -81,6 +81,7 @@ class VCR:
self.path_transformer = path_transformer
self.func_path_generator = func_path_generator
self.decode_compressed_response = decode_compressed_response
self.record_on_exception = record_on_exception
self._custom_patches = tuple(custom_patches)
def _get_serializer(self, serializer_name):
@@ -100,7 +101,7 @@ class VCR:
return matchers
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
# Assume this is an attempt to decorate a function
return self._use_cassette(**kwargs)(function)
@@ -124,6 +125,7 @@ class VCR:
func_path_generator = kwargs.get("func_path_generator", self.func_path_generator)
cassette_library_dir = kwargs.get("cassette_library_dir", self.cassette_library_dir)
additional_matchers = kwargs.get("additional_matchers", ())
record_on_exception = kwargs.get("record_on_exception", self.record_on_exception)
if cassette_library_dir:
@@ -150,6 +152,7 @@ class VCR:
"path_transformer": path_transformer,
"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")
if path:
@@ -216,7 +219,7 @@ class VCR:
filter_functions.extend(before_record_request)
def before_record_request(request):
request = copy.copy(request)
request = copy.deepcopy(request)
for function in filter_functions:
if request is None:
break

View File

@@ -1,8 +1,8 @@
from io import BytesIO
from urllib.parse import urlparse, urlencode, urlunparse
import copy
import json
import zlib
from io import BytesIO
from urllib.parse import urlencode, urlparse, urlunparse
from .util import CaseInsensitiveDict
@@ -150,10 +150,18 @@ def decode_response(response):
"""Returns decompressed body according to encoding using zlib.
to (de-)compress gzip format, use wbits = zlib.MAX_WBITS | 16
"""
if not body:
return ""
if encoding == "gzip":
try:
return zlib.decompress(body, zlib.MAX_WBITS | 16)
except zlib.error:
return body # assumes that the data was already decompressed
else: # encoding == 'deflate'
try:
return zlib.decompress(body)
except zlib.error:
return body # assumes that the data was already decompressed
# Deepcopy here in case `headers` contain objects that could
# be mutated by a shallow copy and corrupt the real response.

View File

@@ -1,43 +1,51 @@
import json
import logging
import urllib
import xmlrpc.client
from .util import read_body
import logging
from .util import read_body
log = logging.getLogger(__name__)
def method(r1, r2):
assert r1.method == r2.method, "{} != {}".format(r1.method, r2.method)
if r1.method != r2.method:
raise AssertionError("{} != {}".format(r1.method, r2.method))
def uri(r1, r2):
assert r1.uri == r2.uri, "{} != {}".format(r1.uri, r2.uri)
if r1.uri != r2.uri:
raise AssertionError("{} != {}".format(r1.uri, r2.uri))
def host(r1, r2):
assert r1.host == r2.host, "{} != {}".format(r1.host, r2.host)
if r1.host != r2.host:
raise AssertionError("{} != {}".format(r1.host, r2.host))
def scheme(r1, r2):
assert r1.scheme == r2.scheme, "{} != {}".format(r1.scheme, r2.scheme)
if r1.scheme != r2.scheme:
raise AssertionError("{} != {}".format(r1.scheme, r2.scheme))
def port(r1, r2):
assert r1.port == r2.port, "{} != {}".format(r1.port, r2.port)
if r1.port != r2.port:
raise AssertionError("{} != {}".format(r1.port, r2.port))
def path(r1, r2):
assert r1.path == r2.path, "{} != {}".format(r1.path, r2.path)
if r1.path != r2.path:
raise AssertionError("{} != {}".format(r1.path, r2.path))
def query(r1, r2):
assert r1.query == r2.query, "{} != {}".format(r1.query, r2.query)
if r1.query != r2.query:
raise AssertionError("{} != {}".format(r1.query, r2.query))
def raw_body(r1, r2):
assert read_body(r1) == read_body(r2)
if read_body(r1) != read_body(r2):
raise AssertionError
def body(r1, r2):
@@ -45,11 +53,13 @@ def body(r1, r2):
r2_transformer = _get_transformer(r2)
if transformer != r2_transformer:
transformer = _identity
assert transformer(read_body(r1)) == transformer(read_body(r2))
if transformer(read_body(r1)) != transformer(read_body(r2)):
raise AssertionError
def headers(r1, r2):
assert r1.headers == r2.headers, "{} != {}".format(r1.headers, r2.headers)
if r1.headers != r2.headers:
raise AssertionError("{} != {}".format(r1.headers, r2.headers))
def _header_checker(value, header="Content-Type"):

View File

@@ -7,7 +7,7 @@ It merges and deletes the request obsolete keys (protocol, host, port, path)
into new 'uri' key.
Usage::
python -m vcr.migration PATH
python3 -m vcr.migration PATH
The PATH can be path to the directory with cassettes or cassette itself
"""
@@ -17,11 +17,12 @@ import os
import shutil
import sys
import tempfile
import yaml
from .serializers import yamlserializer, jsonserializer
from .serialize import serialize
from . import request
from .serialize import serialize
from .serializers import jsonserializer, yamlserializer
from .stubs.compat import get_httpmessage
# Use the libYAML versions if possible
@@ -137,7 +138,7 @@ def try_migrate(path):
def main():
if len(sys.argv) != 2:
raise SystemExit(
"Please provide path to cassettes directory or file. " "Usage: python -m vcr.migration PATH"
"Please provide path to cassettes directory or file. " "Usage: python3 -m vcr.migration PATH"
)
path = sys.argv[1]

View File

@@ -1,13 +1,12 @@
"""Utilities for patching in cassettes"""
import contextlib
import functools
import http.client as httplib
import itertools
import logging
from unittest import mock
from .stubs import VCRHTTPConnection, VCRHTTPSConnection
import http.client as httplib
import logging
log = logging.getLogger(__name__)
# Save some of the original types for the purposes of unpatching
@@ -16,42 +15,47 @@ _HTTPSConnection = httplib.HTTPSConnection
# Try to save the original types for boto3
try:
from botocore.awsrequest import AWSHTTPSConnection, AWSHTTPConnection
except ImportError:
from botocore.awsrequest import AWSHTTPConnection, AWSHTTPSConnection
except ImportError as e:
try:
import botocore.vendored.requests.packages.urllib3.connectionpool as cpool
import botocore.vendored.requests # noqa: F401
except ImportError: # pragma: no cover
pass
else:
_Boto3VerifiedHTTPSConnection = cpool.VerifiedHTTPSConnection
_cpoolBoto3HTTPConnection = cpool.HTTPConnection
_cpoolBoto3HTTPSConnection = cpool.HTTPSConnection
raise RuntimeError(
"vcrpy >=4.2.2 and botocore <1.11.0 are not compatible"
"; please upgrade botocore (or downgrade vcrpy)"
) from e
else:
_Boto3VerifiedHTTPSConnection = AWSHTTPSConnection
_cpoolBoto3HTTPConnection = AWSHTTPConnection
_cpoolBoto3HTTPSConnection = AWSHTTPSConnection
cpool = None
conn = None
# Try to save the original types for urllib3
try:
import urllib3.connection as conn
import urllib3.connectionpool as cpool
except ImportError: # pragma: no cover
pass
else:
_VerifiedHTTPSConnection = cpool.VerifiedHTTPSConnection
_cpoolHTTPConnection = cpool.HTTPConnection
_cpoolHTTPSConnection = cpool.HTTPSConnection
_VerifiedHTTPSConnection = conn.VerifiedHTTPSConnection
_connHTTPConnection = conn.HTTPConnection
_connHTTPSConnection = conn.HTTPSConnection
# Try to save the original types for requests
try:
if not cpool:
import requests.packages.urllib3.connectionpool as cpool
import requests
except ImportError: # pragma: no cover
pass
else:
_VerifiedHTTPSConnection = cpool.VerifiedHTTPSConnection
_cpoolHTTPConnection = cpool.HTTPConnection
_cpoolHTTPSConnection = cpool.HTTPSConnection
if requests.__build__ < 0x021602:
raise RuntimeError(
"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:
@@ -196,27 +200,18 @@ class CassettePatcherBuilder:
from .stubs import requests_stubs
except ImportError: # pragma: no cover
return ()
return self._urllib3_patchers(cpool, requests_stubs)
return self._urllib3_patchers(cpool, conn, requests_stubs)
@_build_patchers_from_mock_triples_decorator
def _boto3(self):
try:
# botocore using awsrequest
import botocore.awsrequest as cpool
except ImportError: # pragma: no cover
try:
# 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:
from .stubs import boto3_stubs
log.debug("Patching boto3 cpool with %s", cpool)
yield cpool.AWSHTTPConnectionPool, "ConnectionCls", boto3_stubs.VCRRequestsHTTPConnection
yield cpool.AWSHTTPSConnectionPool, "ConnectionCls", boto3_stubs.VCRRequestsHTTPSConnection
@@ -255,12 +250,13 @@ class CassettePatcherBuilder:
def _urllib3(self):
try:
import urllib3.connection as conn
import urllib3.connectionpool as cpool
except ImportError: # pragma: no cover
return ()
from .stubs import urllib3_stubs
return self._urllib3_patchers(cpool, urllib3_stubs)
return self._urllib3_patchers(cpool, conn, urllib3_stubs)
@_build_patchers_from_mock_triples_decorator
def _httplib2(self):
@@ -269,8 +265,7 @@ class CassettePatcherBuilder:
except ImportError: # pragma: no cover
pass
else:
from .stubs.httplib2_stubs import VCRHTTPConnectionWithTimeout
from .stubs.httplib2_stubs import VCRHTTPSConnectionWithTimeout
from .stubs.httplib2_stubs import VCRHTTPConnectionWithTimeout, VCRHTTPSConnectionWithTimeout
yield cpool, "HTTPConnectionWithTimeout", VCRHTTPConnectionWithTimeout
yield cpool, "HTTPSConnectionWithTimeout", VCRHTTPSConnectionWithTimeout
@@ -338,7 +333,7 @@ class CassettePatcherBuilder:
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, conn, stubs):
http_connection_remover = ConnectionRemover(
self._get_cassette_subclass(stubs.VCRRequestsHTTPConnection)
)
@@ -346,9 +341,9 @@ class CassettePatcherBuilder:
self._get_cassette_subclass(stubs.VCRRequestsHTTPSConnection)
)
mock_triples = (
(cpool, "VerifiedHTTPSConnection", stubs.VCRRequestsHTTPSConnection),
(cpool, "HTTPConnection", stubs.VCRRequestsHTTPConnection),
(cpool, "HTTPSConnection", stubs.VCRRequestsHTTPSConnection),
(conn, "VerifiedHTTPSConnection", stubs.VCRRequestsHTTPSConnection),
(conn, "HTTPConnection", stubs.VCRRequestsHTTPConnection),
(conn, "HTTPSConnection", stubs.VCRRequestsHTTPSConnection),
(cpool, "is_connection_dropped", mock.Mock(return_value=False)), # Needed on Windows only
(cpool.HTTPConnectionPool, "ConnectionCls", stubs.VCRRequestsHTTPConnection),
(cpool.HTTPSConnectionPool, "ConnectionCls", stubs.VCRRequestsHTTPSConnection),
@@ -418,69 +413,23 @@ def reset_patchers():
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:
import urllib3.connection as conn
import urllib3.connectionpool as cpool
except ImportError: # pragma: no cover
pass
else:
yield mock.patch.object(cpool, "VerifiedHTTPSConnection", _VerifiedHTTPSConnection)
yield mock.patch.object(cpool, "HTTPConnection", _cpoolHTTPConnection)
yield mock.patch.object(cpool, "HTTPSConnection", _cpoolHTTPSConnection)
yield mock.patch.object(conn, "VerifiedHTTPSConnection", _VerifiedHTTPSConnection)
yield mock.patch.object(conn, "HTTPConnection", _connHTTPConnection)
yield mock.patch.object(conn, "HTTPSConnection", _connHTTPSConnection)
if hasattr(cpool.HTTPConnectionPool, "ConnectionCls"):
yield mock.patch.object(cpool.HTTPConnectionPool, "ConnectionCls", _cpoolHTTPConnection)
yield mock.patch.object(cpool.HTTPSConnectionPool, "ConnectionCls", _cpoolHTTPSConnection)
yield mock.patch.object(cpool.HTTPConnectionPool, "ConnectionCls", _connHTTPConnection)
yield mock.patch.object(cpool.HTTPSConnectionPool, "ConnectionCls", _connHTTPSConnection)
try:
# unpatch botocore with awsrequest
import botocore.awsrequest as cpool
except ImportError: # pragma: no cover
try:
# 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:
if hasattr(cpool.AWSHTTPConnectionPool, "ConnectionCls"):
yield mock.patch.object(cpool.AWSHTTPConnectionPool, "ConnectionCls", _cpoolBoto3HTTPConnection)

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,14 +1,12 @@
"""Stubs for patching HTTP and HTTPS requests"""
import logging
from http.client import (
HTTPConnection,
HTTPSConnection,
HTTPResponse,
)
from http.client import HTTPConnection, HTTPResponse, HTTPSConnection
from io import BytesIO
from vcr.request import Request
from vcr.errors import CannotOverwriteExistingCassetteException
from vcr.request import Request
from . import compat
log = logging.getLogger(__name__)
@@ -49,8 +47,9 @@ def parse_headers(header_list):
def serialize_headers(response):
headers = response.headers if response.msg is None else response.msg
out = {}
for key, values in compat.get_headers(response.msg):
for key, values in compat.get_headers(headers):
out.setdefault(key, [])
out[key].extend(values)
return out
@@ -69,6 +68,7 @@ class VCRHTTPResponse(HTTPResponse):
self.version = None
self._content = BytesIO(self.recorded_response["body"]["string"])
self._closed = False
self._original_response = self # for requests.session.Session cookie extraction
headers = self.recorded_response["headers"]
# Since we are loading a response that has already been serialized, our
@@ -93,6 +93,9 @@ class VCRHTTPResponse(HTTPResponse):
def read(self, *args, **kwargs):
return self._content.read(*args, **kwargs)
def read1(self, *args, **kwargs):
return self._content.read1(*args, **kwargs)
def readall(self):
return self._content.readall()
@@ -145,6 +148,35 @@ class VCRHTTPResponse(HTTPResponse):
def readable(self):
return self._content.readable()
@property
def length_remaining(self):
return self._content.getbuffer().nbytes - self._content.tell()
def get_redirect_location(self):
"""
Returns (a) redirect location string if we got a redirect
status code and valid location, (b) None if redirect status and
no location, (c) False if not a redirect status code.
See https://urllib3.readthedocs.io/en/stable/reference/urllib3.response.html .
"""
if not (300 <= self.status <= 399):
return False
return self.getheader("Location")
@property
def data(self):
return self._content.getbuffer().tobytes()
def drain_conn(self):
pass
def stream(self, amt=65536, decode_content=None):
while True:
b = self._content.read(amt)
yield b
if not b:
break
class VCRConnection:
# A reference to the cassette that's currently being patched in
@@ -250,12 +282,13 @@ class VCRConnection:
# get the response
response = self.real_connection.getresponse()
response_data = response.data if hasattr(response, "data") else response.read()
# put the response into the cassette
response = {
"status": {"code": response.status, "message": response.reason},
"headers": serialize_headers(response),
"body": {"string": response.read()},
"body": {"string": response_data},
}
self.cassette.append(self._vcr_request, response)
return VCRHTTPResponse(response)

View File

@@ -1,15 +1,14 @@
"""Stubs for aiohttp HTTP clients"""
import asyncio
import functools
import logging
import json
from aiohttp import ClientConnectionError, ClientResponse, RequestInfo, streams
from aiohttp import hdrs, CookieJar
import logging
from http.cookies import CookieError, Morsel, SimpleCookie
from typing import Mapping, Union
from aiohttp import ClientConnectionError, ClientResponse, CookieJar, RequestInfo, hdrs, streams
from aiohttp.helpers import strip_auth_from_url
from multidict import CIMultiDict, CIMultiDictProxy, MultiDict
from typing import Union, Mapping
from yarl import URL
from vcr.errors import CannotOverwriteExistingCassetteException

View File

@@ -1,17 +1,6 @@
"""Stubs for boto3"""
try:
# boto using awsrequest
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 botocore.awsrequest import AWSHTTPConnection as HTTPConnection
from botocore.awsrequest import AWSHTTPSConnection as VerifiedHTTPSConnection
from ..stubs import VCRHTTPConnection, VCRHTTPSConnection

View File

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

View File

@@ -1,6 +1,5 @@
from io import BytesIO
import http.client
from io import BytesIO
"""
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):
for (key, values) in get_headers(message):
for key, values in get_headers(message):
for value in values:
yield key, value

View File

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

View File

@@ -1,11 +1,12 @@
import functools
import inspect
import logging
from unittest.mock import patch, MagicMock
from unittest.mock import MagicMock, patch
import httpx
from vcr.request import Request as VcrRequest
from vcr.errors import CannotOverwriteExistingCassetteException
import inspect
from vcr.request import Request as VcrRequest
_httpx_signature = inspect.signature(httpx.Client.request)

View File

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

View File

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

39
vcr/unittest.py Normal file
View File

@@ -0,0 +1,39 @@
import inspect
import os
import unittest
from .config import VCR
class VCRMixin:
"""A TestCase mixin that provides VCR integration."""
vcr_enabled = True
def setUp(self):
super().setUp()
if self.vcr_enabled:
kwargs = self._get_vcr_kwargs()
myvcr = self._get_vcr(**kwargs)
cm = myvcr.use_cassette(self._get_cassette_name())
self.cassette = cm.__enter__()
self.addCleanup(cm.__exit__, None, None, None)
def _get_vcr(self, **kwargs):
if "cassette_library_dir" not in kwargs:
kwargs["cassette_library_dir"] = self._get_cassette_library_dir()
return VCR(**kwargs)
def _get_vcr_kwargs(self, **kwargs):
return kwargs
def _get_cassette_library_dir(self):
testdir = os.path.dirname(inspect.getfile(self.__class__))
return os.path.join(testdir, "cassettes")
def _get_cassette_name(self):
return "{0}.{1}.yaml".format(self.__class__.__name__, self._testMethodName)
class VCRTestCase(VCRMixin, unittest.TestCase):
pass