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

Compare commits

...

198 Commits

Author SHA1 Message Date
Kevin McCarthy
34d5384318 bump version to v5.0.0 2023-06-26 12:54:39 -05:00
Sebastian Pipping
ad1010d0f8 Merge pull request #695 from kevin1024/drop37
Drop support for Python 3.7 (after 2023-06-27)
2023-06-26 18:32:42 +02:00
Amos Ng
d99593bcd3 Split persister errors into CassetteNotFoundError and CassetteDecodeError (#681) 2023-06-26 18:27:35 +02:00
Sebastian Pipping
8c03c37df4 Merge pull request #725 from kevin1024/make-assert-is-json-less-misleading
assertions.py: Fix mis-leading `assert_is_json`
2023-06-26 17:43:28 +02:00
Jair Henrique
b827cbe2da Drop support to python 3.7 2023-06-26 11:46:20 -03:00
Kevin McCarthy
92ca5a102c fix misspelled word 2023-06-26 09:22:16 -05:00
Kevin McCarthy
d2281ab646 version bump to v4.4.0 2023-06-26 09:17:41 -05:00
Sebastian Pipping
f21c8f0224 assertions.py: Fix mis-leading assert_is_json
Parameter name "a_string" was mistaken and function
name "assert_is_json" was less clear than ideal,
given that it explicitly needs bytes unlike json.loads .
2023-06-24 15:59:23 +02: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
Kevin McCarthy
f3f66086a1 Release v4.2.1 2022-08-31 14:14:32 -05:00
Ville Skyttä
c3767c2fdb fix(aiohttp): record body of first request in redirect chain
The first could be e.g. a POST with one, only the remaining ones are
expected to be GETs without one.
2022-08-31 14:11:55 -05:00
Joel Shprentz
6fff3ab952 Add match_on option scheme in configuration.rst
Minor documentation corrections: The list of ``match_on`` options was missing ``scheme``. Also, an unneeded period appeared after URI.
2022-08-04 08:42:36 -03:00
Tim Gates
35378d046f docs: Fix a few typos
There are small typos in:
- tests/integration/test_record_mode.py
- tests/integration/test_stubs.py
- vcr/stubs/__init__.py

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

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

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

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

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

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

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-04-19 09:11:17 -03:00
dependabot[bot]
fb065751dc Bump actions/checkout from 3.0.0 to 3.0.1
Bumps [actions/checkout](https://github.com/actions/checkout) from 3.0.0 to 3.0.1.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v3.0.0...v3.0.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-04-19 09:06:03 -03:00
Jair Henrique
874cf06407 Drop support to python 3.6 2022-04-18 17:54:46 -03:00
dependabot[bot]
b0e83986f0 Bump actions/checkout from 2.4.0 to 3.0.0
Bumps [actions/checkout](https://github.com/actions/checkout) from 2.4.0 to 3.0.0.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v2.4.0...v3.0.0)

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

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

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

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

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

Update changelog

Revert accidental import deletion

* Remove move dependency that was masking error from tox.ini
2019-12-20 22:08:25 +11:00
Josh Peak
f07083e7cc Fix Logo alignment for PYPI description 2019-12-20 12:11:01 +11:00
Josh Peak
4495dbd1ce Fixed README syntax errors thanks to and 2019-12-20 12:04:36 +11:00
Josh Peak
357e3b03c5 Revert to PNG for PYPI upload 2019-12-20 11:55:36 +11:00
Josh Peak
566a70ab3a Updated link to logo in README so it animates 2019-12-20 11:48:08 +11:00
Josh Peak
4a8e80ee3e v4.0.x - Remove legacy python and add python3.8 support (#499)
* Drop support for legacy Python 2.7

* Upgrade Python syntax with pyupgrade --py3-plus

* Trim testing matrix to remove python2

* re-enable python3.8 in travis as tests that are not allowed to fail

* Remove some six

* The future is now

* Remove Python 2 imports

* Add back example, but change py27 to py36

* Remove redundant compat.py

* Blacken

* Credit hugovk in changelog

WIP Updating Sphinx Docs and AutoDoc

* Fix AutoDoc and update Sphinx theme to python_doc_theme

* Fix #420, autodoc even undocumented (docstring-less) method signatures

* Doc theme 'nature'. Add global TOC to doc sidebar

* Comment last reference to package six

* Changelog is now a consistent format

* Yet another documentation fix for links and title hierarchy

* Start work on new SVG logo

test SVG in README

trying to test new SVG logo in README

Apply centering

Apply readme logo centering

Trying to align image

Trying random shit

trying align right

add emoji

Large logo has higher priority

Change title hierarchy

Actually use a H1

Try again

try and organise badges

revert link back to point at master

* updated new take on VCR logo as SVG code

* Testing modern logo in docs

* Add sanitize for rendering SVG

* Switch to alabaster theme

* Update vcrpy logo (#503)

* Add credit for V4 logo changes.

* Add rewind and play animation

* Add svg into ReadTheDocs static assets so that it can be hosted so the animations work.

* Need to embedd the SVG for ReadTheDocs somewhere so I can get the link to later embed in the README

Co-authored-by: Hugo van Kemenade <hugovk@users.noreply.github.com>
Co-authored-by: Sean Bailey <sean@seanbailey.io>
2019-12-20 11:45:07 +11:00
Josh Peak
41ca5750e7 Fix heading hierarchy in debugging guide 2019-12-19 13:16:12 +11:00
Josh Peak
d91a53bbff Fix italics emphasis in contributing guide 2019-12-19 13:15:19 +11:00
Josh Peak
88092eb3cd Update contributing guide to explain non-code contributions 2019-12-19 13:14:17 +11:00
Josh Peak
4fcc5472bc Update changelog.rst
Correct format of link to milestones for documentation
2019-12-14 20:37:01 +11:00
90 changed files with 2747 additions and 1059 deletions

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

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

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

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

@@ -0,0 +1,43 @@
name: Test
on:
push:
branches:
- master
pull_request:
workflow_dispatch:
jobs:
build:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: ["3.8", "3.9", "3.10", "3.11", "pypy-3.8"]
steps:
- uses: actions/checkout@v3.5.2
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
- name: Install project dependencies
run: |
pip3 install --upgrade pip
pip3 install codecov tox tox-gh-actions
- 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

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

View File

@@ -1,10 +1,16 @@
###########
VCR.py 📼
###########
|PyPI| |Python versions| |Build Status| |CodeCov| |Gitter| |CodeStyleBlack|
VCR.py
======
----
.. image:: https://vcrpy.readthedocs.io/en/latest/_images/vcr.svg
:alt: vcr.py logo
.. image:: https://raw.github.com/kevin1024/vcrpy/master/vcr.png
:alt: vcr.py
This is a Python version of `Ruby's VCR
library <https://github.com/vcr/vcr>`__.
@@ -41,8 +47,13 @@ VCR.py will detect the absence of a cassette file and once again record
all HTTP interactions, which will update them to correspond to the new
API.
Usage with Pytest
-----------------
There is a library to provide some pytest fixtures called pytest-recording https://github.com/kiwicom/pytest-recording
License
=======
-------
This library uses the MIT license. See `LICENSE.txt <LICENSE.txt>`__ for
more details
@@ -51,8 +62,8 @@ more details
:target: https://pypi.python.org/pypi/vcrpy
.. |Python versions| image:: https://img.shields.io/pypi/pyversions/vcrpy.svg
:target: https://pypi.python.org/pypi/vcrpy
.. |Build Status| image:: https://secure.travis-ci.org/kevin1024/vcrpy.svg?branch=master
:target: http://travis-ci.org/kevin1024/vcrpy
.. |Build Status| image:: https://github.com/kevin1024/vcrpy/actions/workflows/main.yml/badge.svg
:target: https://github.com/kevin1024/vcrpy/actions
.. |Gitter| image:: https://badges.gitter.im/Join%20Chat.svg
:alt: Join the chat at https://gitter.im/kevin1024/vcrpy
:target: https://gitter.im/kevin1024/vcrpy?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge

BIN
docs/_static/vcr.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

27
docs/_static/vcr.svg vendored Normal file
View File

@@ -0,0 +1,27 @@
<svg width="634" height="346" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0 10C0 4.477 4.477 0 10 0h614c5.523 0 10 4.477 10 10v326c0 5.523-4.477 10-10 10H10c-5.523 0-10-4.477-10-10V10z" fill="#181B19"/>
<rect x="36" y="83" width="561" height="234" rx="19" fill="#262927"/>
<mask id="a" maskUnits="userSpaceOnUse" x="66" y="104" width="503" height="192">
<path d="M161.434 120h310.431m-229.757 80c0 44.183-35.841 80-80.054 80S82 244.183 82 200s35.841-80 80.054-80 80.054 35.817 80.054 80zm310.432 0c0 44.183-35.841 80-80.054 80s-80.054-35.817-80.054-80 35.841-80 80.054-80 80.054 35.817 80.054 80z" stroke="#27DD7C" stroke-width="31" stroke-dasharray="503" stroke-dashoffset="0">
<animate attributeName="stroke-dashoffset" to="503" begin="1s" dur="1s"/>
<animate attributeName="stroke" to="#0" begin="1s" dur="1s"/>
<animate attributeName="stroke-dashoffset" from="503" to="0" begin="2s" dur="3s" />
<animate attributeName="stroke" from="#000" to="#27DD7C" begin="2s" dur="3s"/>
</path>
</mask>
<g mask="url(#a)">
<path fill="url(#paint0_linear)" d="M64 102h507v196H64z"/>
</g>
<path d="M0 10C0 4.477 4.477 0 10 0h614c5.523 0 10 4.477 10 10v40H0V10z" fill="#262927"/>
<rect x="189" y="6" width="256" height="38" rx="2" fill="#fff"/>
<path stroke="#E6E8E6" d="M198 14.5h238M198 25.5h238M198 36.5h238"/>
<path d="M261.207 18.61c-.443 0-.762-.098-.957-.294-.182-.195-.273-.54-.273-1.035 0-.494.091-.84.273-1.035.195-.195.514-.293.957-.293h6.914c.443 0 .755.098.938.293.195.195.293.54.293 1.035 0 .495-.098.84-.293 1.035-.183.196-.495.293-.938.293h-2.5l5.332 12.735 5.391-12.735h-1.699c-.443 0-.762-.097-.957-.293-.183-.195-.274-.54-.274-1.035 0-.494.091-.84.274-1.035.195-.195.514-.293.957-.293h6.132c.443 0 .756.098.938.293.195.195.293.54.293 1.035 0 .495-.098.84-.293 1.035-.182.196-.495.293-.938.293h-1.23l-6.309 14.551c-.182.443-.449.762-.8.957-.352.209-.853.313-1.504.313s-1.146-.105-1.485-.313c-.338-.208-.599-.527-.781-.957l-6.25-14.55h-1.211zm38.136 3.847a3.73 3.73 0 00-.352-1.621 3.392 3.392 0 00-1.054-1.27c-.456-.364-1.022-.644-1.7-.84-.677-.208-1.464-.312-2.363-.312-.95 0-1.829.163-2.637.488a6.03 6.03 0 00-2.05 1.367 6.354 6.354 0 00-1.348 2.09c-.325.795-.488 1.667-.488 2.618 0 .95.15 1.829.449 2.636a6.197 6.197 0 001.27 2.07 5.858 5.858 0 002.011 1.368c.781.325 1.647.488 2.598.488 1.198 0 2.357-.182 3.476-.547a10.295 10.295 0 003.086-1.62c.339-.261.658-.359.957-.294.3.065.586.326.86.781.221.352.312.716.273 1.094-.026.378-.228.703-.605.977-1.055.78-2.285 1.393-3.692 1.836a14.417 14.417 0 01-4.355.664c-1.432 0-2.728-.235-3.887-.703-1.159-.482-2.148-1.146-2.969-1.993a8.805 8.805 0 01-1.894-2.988c-.443-1.159-.664-2.415-.664-3.77 0-1.354.228-2.604.683-3.75a9.008 9.008 0 011.914-3.007 8.783 8.783 0 012.969-1.973c1.159-.482 2.442-.723 3.848-.723 1.185 0 2.259.215 3.223.645a6.591 6.591 0 012.441 1.797v-1.172c0-.456.104-.781.312-.977.222-.195.606-.293 1.153-.293s.924.098 1.133.293c.221.196.332.521.332.977v5.664c0 .456-.111.781-.332.977-.209.195-.586.293-1.133.293s-.931-.098-1.153-.293c-.208-.196-.312-.521-.312-.977zm12.394-3.848h-2.481c-.442 0-.761-.097-.957-.293-.182-.195-.273-.54-.273-1.035 0-.494.091-.84.273-1.035.196-.195.515-.293.957-.293h3.985c.442 0 .755.098.937.293.195.195.293.54.293 1.035v3.946a13.604 13.604 0 011.641-2.364 11.627 11.627 0 011.992-1.797 8.754 8.754 0 012.187-1.132 6.569 6.569 0 012.246-.41 5.53 5.53 0 012.852.78c.378.222.619.495.723.821.104.326.065.73-.117 1.21-.183.47-.417.802-.704.997-.273.195-.599.182-.976-.039a4.257 4.257 0 00-2.051-.527c-.586 0-1.185.11-1.797.332a7.489 7.489 0 00-1.758.898c-.56.378-1.087.84-1.582 1.387a10.586 10.586 0 00-1.289 1.777 9.542 9.542 0 00-.859 2.09 8.5 8.5 0 00-.313 2.305v3.789h6.582c.443 0 .756.097.938.293.195.195.293.54.293 1.035 0 .495-.098.84-.293 1.035-.182.195-.495.293-.938.293H308.67c-.442 0-.761-.098-.957-.293-.182-.195-.273-.54-.273-1.035 0-.495.091-.84.273-1.035.196-.196.515-.293.957-.293h3.067V18.609zm19.561 0h-1.601c-.443 0-.762-.097-.957-.293-.182-.195-.274-.54-.274-1.035 0-.494.092-.84.274-1.035.195-.195.514-.293.957-.293h3.105c.443 0 .756.098.938.293.195.195.293.54.293 1.035v1.563c.807-1.055 1.745-1.869 2.812-2.442 1.081-.586 2.272-.879 3.575-.879 1.21 0 2.33.209 3.359.625a7.4 7.4 0 012.656 1.758c.755.768 1.348 1.706 1.778 2.813.429 1.107.644 2.363.644 3.77 0 1.405-.215 2.662-.644 3.769-.43 1.107-1.023 2.044-1.778 2.812a7.62 7.62 0 01-2.656 1.778c-1.029.403-2.149.605-3.359.605-1.303 0-2.468-.26-3.497-.781-1.015-.534-1.914-1.29-2.695-2.266v8.614h4.336c.443 0 .755.097.938.292.195.196.293.54.293 1.036 0 .494-.098.84-.293 1.035-.183.195-.495.293-.938.293h-9.453c-.443 0-.762-.098-.957-.293-.182-.196-.274-.54-.274-1.035 0-.495.092-.84.274-1.035.195-.196.514-.294.957-.294h2.187V18.61zm2.93 5.88c0 .963.15 1.822.449 2.577.3.756.716 1.394 1.25 1.915a5.536 5.536 0 001.895 1.171c.742.274 1.543.41 2.402.41.886 0 1.673-.15 2.364-.449.69-.3 1.269-.716 1.738-1.25a5.696 5.696 0 001.074-1.933 7.692 7.692 0 00.371-2.442c0-.885-.124-1.699-.371-2.441a5.485 5.485 0 00-1.074-1.914 4.738 4.738 0 00-1.738-1.27c-.691-.3-1.478-.449-2.364-.449-.859 0-1.66.137-2.402.41a5.391 5.391 0 00-1.895 1.192c-.534.507-.95 1.139-1.25 1.894-.299.755-.449 1.615-.449 2.578zm37.687-8.536c.443 0 .755.098.937.293.196.195.293.54.293 1.035 0 .495-.097.84-.293 1.035-.182.196-.494.293-.937.293h-1.211l-7.988 16.368c-.625 1.276-1.237 2.356-1.836 3.242-.599.885-1.244 1.608-1.934 2.168s-1.452.97-2.285 1.23c-.82.274-1.764.43-2.832.469-.378.013-.697-.072-.957-.254-.248-.182-.397-.534-.449-1.055-.052-.573.013-.983.195-1.23.195-.247.469-.384.82-.41.899-.052 1.66-.176 2.285-.371a4.375 4.375 0 001.739-.977c.521-.469 1.028-1.12 1.523-1.953.508-.82 1.087-1.888 1.738-3.203l-7.148-14.024h-1.23c-.443 0-.762-.097-.958-.293-.182-.195-.273-.54-.273-1.035 0-.494.091-.84.273-1.035.196-.195.515-.293.958-.293h6.914c.442 0 .755.098.937.293.195.195.293.54.293 1.035 0 .495-.098.84-.293 1.035-.182.196-.495.293-.937.293h-2.5l5.507 10.977 5.254-10.977h-1.738c-.443 0-.762-.097-.957-.293-.182-.195-.273-.54-.273-1.035 0-.494.091-.84.273-1.035.195-.195.514-.293.957-.293h6.133z" fill="#262927"/>
<defs>
<linearGradient id="paint0_linear" x1="64" y1="298" x2="544.524" y2="50.579" gradientUnits="userSpaceOnUse">
<stop stop-color="#27DD70"/>
<stop offset="1" stop-color="#27DDA6"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 6.2 KiB

View File

@@ -33,6 +33,8 @@ consider part of the API. The fields are as follows:
been played back.
- ``responses_of(request)``: Access the responses that match a given
request
- ``allow_playback_repeats``: A boolean indicating whether responses
can be played back more than once.
The ``Request`` object has the following properties:
@@ -69,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
"""
@@ -130,11 +132,12 @@ Finally, register your method with VCR to use your new request matcher.
Register your own cassette persister
------------------------------------
Create your own persistence class, see the :ref:`persister_example`.
Create your own persistence class, see the example below:
Your custom persister must implement both ``load_cassette`` and ``save_cassette``
methods. The ``load_cassette`` method must return a deserialized cassette or raise
``ValueError`` if no cassette is found.
either ``CassetteNotFoundError`` if no cassette is found, or ``CassetteDecodeError``
if the cassette cannot be successfully deserialized.
Once the persister class is defined, register with VCR like so...
@@ -143,7 +146,7 @@ Once the persister class is defined, register with VCR like so...
import vcr
my_vcr = vcr.VCR()
class CustomerPersister(object):
class CustomerPersister:
# implement Persister methods...
my_vcr.register_persister(CustomPersister)
@@ -269,7 +272,7 @@ You can also do response filtering with the
similar to the above ``before_record_request`` - you can
mutate the response, or return ``None`` to avoid recording
the request and response altogether. For example to hide
sensitive data from the request body:
sensitive data from the response body:
.. code:: python
@@ -386,3 +389,41 @@ VCR.py allows to rewind a cassette in order to replay it inside the same functio
assert cass.all_played
cass.rewind()
assert not cass.all_played
Playback Repeats
----------------
By default, each response in a cassette can only be matched and played back
once while the cassette is in use, unless the cassette is rewound.
If you want to allow playback repeats without rewinding the cassette, use
the Cassette ``allow_playback_repeats`` option.
.. code:: python
with vcr.use_cassette('fixtures/vcr_cassettes/synopsis.yaml', allow_playback_repeats=True) as cass:
for x in range(10):
response = urllib2.urlopen('http://www.zombo.com/').read()
assert cass.all_played
Discards Cassette on Errors
---------------------------
By default VCR will save the cassette file even when there is any error inside
the enclosing context/test.
If you want to save the cassette only when the test succeeds, set the Cassette
``record_on_exception`` option to ``False``.
.. code:: python
try:
my_vcr = VCR(record_on_exception=False)
with my_vcr.use_cassette('fixtures/vcr_cassettes/synopsis.yaml') as cass:
response = urllib2.urlopen('http://www.zombo.com/').read()
raise RuntimeError("Oops, something happened")
except RuntimeError:
pass
# Since there was an exception, the cassette file hasn't been created.
assert not os.path.exists('fixtures/vcr_cassettes/synopsis.yaml')

View File

@@ -1,210 +1,276 @@
Changelog
---------
For a full list of triaged issues, bugs and PRs and what release they are targetted for please see the following link.
For a full list of triaged issues, bugs and PRs and what release they are targeted for please see the following link.
`ROADMAP MILESTONES <https://github.com/kevin1024/vcrpy/milestones>`
`ROADMAP MILESTONES <https://github.com/kevin1024/vcrpy/milestones>`_
All help in providing PRs to close out bug issues is appreciated. Even if that is providing a repo that fully replicates issues. We have very generous contributors that have added these to bug issues which meant another contributor picked up the bug and closed it out.
- 4.0.0 (UNRELEASED)
- Remove Python2 support
- Add Python 3.8 TravisCI support
- 5.0.0
- BREAKING CHANGE: Drop support for Python 3.7. 3.7 is EOL as of 6/27/23 Thanks @jairhenrique
- BREAKING CHANGE: Custom Cassette persisters no longer catch ValueError. If you have implemented a custom persister (has anyone implemented a custom persister? Let us know!) then you will need to throw a CassetteNotFoundError when unable to find a cassette. See #681 for discussion and reason for this change. Thanks @amosjyng for the PR and the review from @hartwork
- 4.4.0
- HUGE thanks to @hartwork for all the work done on this release!
- Bring vcr/unittest in to vcrpy as a full feature of vcr instead of a separate library. Big thanks to @hartwork for doing this and to @agriffis for originally creating the library
- Make decompression robust towards already decompressed input (thanks @hartwork)
- Bugfix: Add read1 method (fixes compatibility 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
- 4.2.0
- Drop support for python < 3.7, thanks @jairhenrique, @IvanMalison, @AthulMuralidhar
- Various aiohtt bigfixes (thanks @pauloromeira and boechat107)
- Bugfix: filter_post_data_parameters not working with aiohttp. Thank you @vprakashplanview, @scop, @jairhenrique, and @cinemascop89
- Bugfix: Some random misspellings (thanks @scop)
- Migrate the CI suite to Github Actions from Travis (thanks @jairhenrique and @cclauss)
- Various documentation and code misspelling fixes (thanks @scop and @Justintime50)
- Bugfix: httpx support (select between allow_redirects/follow_redirects) (thanks @immerrr)
- Bugfix: httpx support (select between allow_redirects/follow_redirects) (thanks @immerrr)
- 4.1.1
- Fix HTTPX support for versions greater than 0.15 (thanks @jairhenrique)
- Include a trailing newline on json cassettes (thanks @AaronRobson)
- 4.1.0
- Add support for httpx!! (thanks @herdigiorgi)
- Add the new `allow_playback_repeats` option (thanks @tysonholub)
- Several aiohttp improvements (cookie support, multiple headers with same key) (Thanks @pauloromeira)
- Use enums for record modes (thanks @aaronbannin)
- Bugfix: Do not redirect on 304 in aiohttp (Thanks @royjs)
- Bugfix: Fix test suite by switching to mockbin (thanks @jairhenrique)
- 4.0.2
- Fix mock imports as reported in #504 by @llybin. Thank you.
- 4.0.1
- Fix logo alignment for PyPI
- 4.0.0
- Remove Python2 support (@hugovk)
- Add Python 3.8 TravisCI support (@neozenith)
- Updated the logo to a modern material design (@sean0x42)
- 3.0.0
- This release is a breaking change as it changes how aiohttp follows redirects and your cassettes may need to be re-recorded with this update.
- Fix multiple requests being replayed per single request in aiohttp stub #495 (@nickdirienzo)
- Add support for `request_info` on mocked responses in aiohttp stub #495 (@nickdirienzo)
- doc: fixed variable name (a -> cass) in an example for rewind #492 (@yarikoptic)
- This release is a breaking change as it changes how aiohttp follows redirects and your cassettes may need to be re-recorded with this update.
- Fix multiple requests being replayed per single request in aiohttp stub #495 (@nickdirienzo)
- Add support for `request_info` on mocked responses in aiohttp stub #495 (@nickdirienzo)
- doc: fixed variable name (a -> cass) in an example for rewind #492 (@yarikoptic)
- 2.1.1
- Format code with black (@neozenith)
- Use latest pypy3 in Travis (@hugovk)
- Improve documentation about custom matchers (@gward)
- Fix exception when body is empty (@keithprickett)
- Add `pytest-recording` to the documentation as an alternative Pytest plugin (@Stranger6667)
- Fix yarl and python3.5 version issue (@neozenith)
- Fix header matcher for boto3 - fixes #474 (@simahawk)
- 2.1.0 - Add a `rewind` method to reset a cassette (thanks @khamidou)
New error message with more details on why the cassette failed to play a request (thanks @arthurHamon2, @neozenith)
Handle connect tunnel URI (thanks @jeking3)
Add code coverage to the project (thanks @neozenith)
Drop support to python 3.4
Add deprecation warning on python 2.7, next major release will drop python 2.7 support
Fix build problems on requests tests (thanks to @dunossauro)
Fix matching on 'body' failing when Unicode symbols are present in them (thanks @valgur)
Fix bugs on aiohttp integration (thanks @graingert, @steinnes, @stj, @lamenezes, @lmazuel)
Fix Biopython incompatibility (thanks @rishab121)
Fix Boto3 integration (thanks @1oglop1, @arthurHamon2)
- 2.0.1 - Fix bug when using vcrpy with python 3.4
- 2.0.0 - Support python 3.7 (fix httplib2 and urllib2, thanks @felixonmars)
[#356] Fixes `before_record_response` so the original response isn't changed (thanks @kgraves)
Fix requests stub when using proxy (thanks @samuelfekete @daneoshiga)
(only for aiohttp stub) Drop support to python 3.4 asyncio.coroutine (aiohttp doesn't support python it anymore)
Fix aiohttp stub to work with aiohttp client (thanks @stj)
Fix aiohttp stub to accept content type passed
Improve docs (thanks @adamchainz)
- 1.13.0 - Fix support to latest aiohttp version (3.3.2). Fix content-type bug in aiohttp stub. Save URL with query params properly when using aiohttp.
- 1.12.0 - Fix support to latest aiohttp version (3.2.1), Adapted setup to PEP508, Support binary responses on aiohttp, Dropped support for EOL python versions (2.6 and 3.3)
- 1.11.1 Fix compatibility with newest requests and urllib3 releases
- 1.11.0 Allow injection of persistence methods + bugfixes (thanks @j-funk and @IvanMalison),
Support python 3.6 + CI tests (thanks @derekbekoe and @graingert),
Support pytest-asyncio coroutines (thanks @graingert)
- 1.10.5 Added a fix to httplib2 (thanks @carlosds730), Fix an issue with
aiohttp (thanks @madninja), Add missing requirement yarl (thanks @lamenezes),
Remove duplicate mock triple (thanks @FooBarQuaxx)
- 1.10.4 Fix an issue with asyncio aiohttp (thanks @madninja)
- 1.10.3 Fix some issues with asyncio and params (thanks @anovikov1984 and
@lamenezes), Fix some issues with cassette serialize / deserialize and empty
response bodies (thanks @gRoussac and @dz0ny)
- 1.10.2 Fix 1.10.1 release - add aiohttp support back in
- 1.10.1 [bad release] Fix build for Fedora package + python2 (thanks @puiterwijk and @lamenezes)
- 1.10.0 Add support for aiohttp (thanks @lamenezes)
- 1.9.0 Add support for boto3 (thanks @desdm, @foorbarna). Fix deepcopy issue
for response headers when `decode_compressed_response` is enabled (thanks
@nickdirienzo)
- 1.8.0 Fix for Serialization errors with JSON adapter (thanks
@aliaksandrb). Avoid concatenating bytes with strings (thanks
@jaysonsantos). Exclude __pycache__ dirs & compiled files in sdist
(thanks @koobs). Fix Tornado support behavior for Tornado 3 (thanks
@abhinav). decode_compressed_response option and filter (thanks
@jayvdb).
- 1.7.4 [#217] Make use_cassette decorated functions actually return a
value (thanks @bcen). [#199] Fix path transfromation defaults.
Better headers dictionary management.
- 1.7.3 [#188] ``additional_matchers`` kwarg on ``use_cassette``.
[#191] Actually support passing multiple before_record_request
functions (thanks @agriffis).
- 1.7.2 [#186] Get effective_url in tornado (thanks @mvschaik), [#187]
Set request_time on Response object in tornado (thanks @abhinav).
- 1.7.1 [#183] Patch ``fetch_impl`` instead of the entire HTTPClient
class for Tornado (thanks @abhinav).
- 1.7.0 [#177] Properly support coroutine/generator decoration. [#178]
Support distribute (thanks @graingert). [#163] Make compatibility
between python2 and python3 recorded cassettes more robust (thanks
@gward).
- 1.6.1 [#169] Support conditional requirements in old versions of
pip, Fix RST parse errors generated by pandoc, [Tornado] Fix
unsupported features exception not being raised, [#166]
content-aware body matcher.
- 1.6.0 [#120] Tornado support (thanks @abhinav), [#147] packaging fixes
(thanks @graingert), [#158] allow filtering post params in requests
(thanks @MrJohz), [#140] add xmlrpclib support (thanks @Diaoul).
- 1.5.2 Fix crash when cassette path contains cassette library
directory (thanks @gazpachoking).
- 1.5.0 Automatic cassette naming and 'application/json' post data
filtering (thanks @marco-santamaria).
- 1.4.2 Fix a bug caused by requests 2.7 and chunked transfer encoding
- 1.4.1 Include README, tests, LICENSE in package. Thanks @ralphbean.
- 1.4.0 Filter post data parameters (thanks @eadmundo), support for
posting files through requests, inject\_cassette kwarg to access
cassette from ``use_cassette`` decorated function,
``with_current_defaults`` actually works (thanks @samstav).
- 1.3.0 Fix/add support for urllib3 (thanks @aisch), fix default port
for https (thanks @abhinav).
- 1.2.0 Add custom\_patches argument to VCR/Cassette objects to allow
users to stub custom classes when cassettes become active.
- 1.1.4 Add force reset around calls to actual connection from stubs,
to ensure compatibility with the version of httplib/urlib2 in python
2.7.9.
- 1.1.3 Fix python3 headers field (thanks @rtaboada), fix boto test
(thanks @telaviv), fix new\_episodes record mode (thanks @jashugan),
fix Windows connectionpool stub bug (thanks @gazpachoking), add
support for requests 2.5
- 1.1.2 Add urllib==1.7.1 support. Make json serialize error handling
correct Improve logging of match failures.
- 1.1.1 Use function signature preserving ``wrapt.decorator`` to write
the decorator version of use\_cassette in order to ensure
compatibility with py.test fixtures and python 2. Move all request
filtering into the ``before_record_callable``.
- 1.1.0 Add ``before_record_response``. Fix several bugs related to the
context management of cassettes.
- 1.0.3: Fix an issue with requests 2.4 and make sure case sensitivity
is consistent across python versions
- 1.0.2: Fix an issue with requests 2.3
- 1.0.1: Fix a bug with the new ignore requests feature and the once
record mode
- 1.0.0: *BACKWARDS INCOMPATIBLE*: Please see the 'upgrade' section in
the README. Take a look at the matcher section as well, you might
want to update your ``match_on`` settings. Add support for filtering
sensitive data from requests, matching query strings after the order
changes and improving the built-in matchers, (thanks to @mshytikov),
support for ignoring requests to certain hosts, bump supported
Python3 version to 3.4, fix some bugs with Boto support (thanks
@marusich), fix error with URL field capitalization in README (thanks
@simon-weber), added some log messages to help with debugging, added
``all_played`` property on cassette (thanks @mshytikov)
- 0.7.0: VCR.py now supports Python 3! (thanks @asundg) Also I
refactored the stub connections quite a bit to add support for the
putrequest and putheader calls. This version also adds support for
httplib2 (thanks @nilp0inter). I have added a couple tests for boto
since it is an http client in its own right. Finally, this version
includes a fix for a bug where requests wasn't being patched properly
(thanks @msabramo).
- 0.6.0: Store response headers as a list since a HTTP response can
have the same header twice (happens with set-cookie sometimes). This
has the added benefit of preserving the order of headers. Thanks
@smallcode for the bug report leading to this change. I have made an
effort to ensure backwards compatibility with the old cassettes'
header storage mechanism, but if you want to upgrade to the new
header storage, you should delete your cassettes and re-record them.
Also this release adds better error messages (thanks @msabramo) and
adds support for using VCR as a decorator (thanks @smallcode for the
motivation)
- 0.5.0: Change the ``response_of`` method to ``responses_of`` since
cassettes can now contain more than one response for a request. Since
this changes the API, I'm bumping the version. Also includes 2
bugfixes: a better error message when attempting to overwrite a
cassette file, and a fix for a bug with requests sessions (thanks
@msabramo)
- 0.4.0: Change default request recording behavior for multiple
requests. If you make the same request multiple times to the same
URL, the response might be different each time (maybe the response
has a timestamp in it or something), so this will make the same
request multiple times and save them all. Then, when you are
replaying the cassette, the responses will be played back in the same
order in which they were received. If you were making multiple
requests to the same URL in a cassette before version 0.4.0, you
might need to regenerate your cassette files. Also, removes support
for the cassette.play\_count counter API, since individual requests
aren't unique anymore. A cassette might contain the same request
several times. Also removes secure overwrite feature since that was
breaking overwriting files in Windows, and fixes a bug preventing
request's automatic body decompression from working.
- 0.3.5: Fix compatibility with requests 2.x
- 0.3.4: Bugfix: close file before renaming it. This fixes an issue on
Windows. Thanks @smallcode for the fix.
- 0.3.3: Bugfix for error message when an unreigstered custom matcher
was used
- 0.3.2: Fix issue with new config syntax and the ``match_on``
parameter. Thanks, @chromy!
- 0.3.1: Fix issue causing full paths to be sent on the HTTP request
line.
- 0.3.0: *Backwards incompatible release* - Added support for record
modes, and changed the default recording behavior to the "once"
record mode. Please see the documentation on record modes for more.
Added support for custom request matching, and changed the default
request matching behavior to match only on the URL and method. Also,
improved the httplib mocking to add support for the
``HTTPConnection.send()`` method. This means that requests won't
actually be sent until the response is read, since I need to record
the entire request in order to match up the appropriate response. I
don't think this should cause any issues unless you are sending
requests without ever loading the response (which none of the
standard httplib wrappers do, as far as I know. Thanks to @fatuhoku
for some of the ideas and the motivation behind this release.
- 0.2.1: Fixed missing modules in setup.py
- 0.2.0: Added configuration API, which lets you configure some
settings on VCR (see the README). Also, VCR no longer saves cassettes
if they haven't changed at all and supports JSON as well as YAML
(thanks @sirpengi). Added amazing new skeumorphic logo, thanks
@hairarrow.
- 0.1.0: *backwards incompatible release - delete your old cassette
files*: This release adds the ability to access the cassette to make
assertions on it, as well as a major code refactor thanks to
@dlecocq. It also fixes a couple longstanding bugs with redirects and
HTTPS. [#3 and #4]
- 0.0.4: If you have libyaml installed, vcrpy will use the c bindings
instead. Speed up your tests! Thanks @dlecocq
- 0.0.3: Add support for requests 1.2.3. Support for older versions of
requests dropped (thanks @vitormazzi and @bryanhelmig)
- 0.0.2: Add support for requests / urllib3
- 0.0.1: Initial Release
- Format code with black (@neozenith)
- Use latest pypy3 in Travis (@hugovk)
- Improve documentation about custom matchers (@gward)
- Fix exception when body is empty (@keithprickett)
- Add `pytest-recording` to the documentation as an alternative Pytest plugin (@Stranger6667)
- Fix yarl and python3.5 version issue (@neozenith)
- Fix header matcher for boto3 - fixes #474 (@simahawk)
- 2.1.0
- Add a `rewind` method to reset a cassette (thanks @khamidou)
- New error message with more details on why the cassette failed to play a request (thanks @arthurHamon2, @neozenith)
- Handle connect tunnel URI (thanks @jeking3)
- Add code coverage to the project (thanks @neozenith)
- Drop support to python 3.4
- Add deprecation warning on python 2.7, next major release will drop python 2.7 support
- Fix build problems on requests tests (thanks to @dunossauro)
- Fix matching on 'body' failing when Unicode symbols are present in them (thanks @valgur)
- Fix bugs on aiohttp integration (thanks @graingert, @steinnes, @stj, @lamenezes, @lmazuel)
- Fix Biopython incompatibility (thanks @rishab121)
- Fix Boto3 integration (thanks @1oglop1, @arthurHamon2)
- 2.0.1
- Fix bug when using vcrpy with python 3.4
- 2.0.0
- Support python 3.7 (fix httplib2 and urllib2, thanks @felixonmars)
- [#356] Fixes `before_record_response` so the original response isn't changed (thanks @kgraves)
- Fix requests stub when using proxy (thanks @samuelfekete @daneoshiga)
- (only for aiohttp stub) Drop support to python 3.4 asyncio.coroutine (aiohttp doesn't support python it anymore)
- Fix aiohttp stub to work with aiohttp client (thanks @stj)
- Fix aiohttp stub to accept content type passed
- Improve docs (thanks @adamchainz)
- 1.13.0
- Fix support to latest aiohttp version (3.3.2). Fix content-type bug in aiohttp stub. Save URL with query params properly when using aiohttp.
- 1.12.0
- Fix support to latest aiohttp version (3.2.1), Adapted setup to PEP508, Support binary responses on aiohttp, Dropped support for EOL python versions (2.6 and 3.3)
- 1.11.1
- Fix compatibility with newest requests and urllib3 releases
- 1.11.0
- Allow injection of persistence methods + bugfixes (thanks @j-funk and @IvanMalison),
- Support python 3.6 + CI tests (thanks @derekbekoe and @graingert),
- Support pytest-asyncio coroutines (thanks @graingert)
- 1.10.5
- Added a fix to httplib2 (thanks @carlosds730), Fix an issue with
- aiohttp (thanks @madninja), Add missing requirement yarl (thanks @lamenezes),
- Remove duplicate mock triple (thanks @FooBarQuaxx)
- 1.10.4
- Fix an issue with asyncio aiohttp (thanks @madninja)
- 1.10.3
- Fix some issues with asyncio and params (thanks @anovikov1984 and @lamenezes)
- Fix some issues with cassette serialize / deserialize and empty response bodies (thanks @gRoussac and @dz0ny)
- 1.10.2
- Fix 1.10.1 release - add aiohttp support back in
- 1.10.1
- [bad release] Fix build for Fedora package + python2 (thanks @puiterwijk and @lamenezes)
- 1.10.0
- Add support for aiohttp (thanks @lamenezes)
- 1.9.0
- Add support for boto3 (thanks @desdm, @foorbarna).
- Fix deepcopy issue for response headers when `decode_compressed_response` is enabled (thanks @nickdirienzo)
- 1.8.0
- Fix for Serialization errors with JSON adapter (thanks @aliaksandrb).
- Avoid concatenating bytes with strings (thanks @jaysonsantos).
- Exclude __pycache__ dirs & compiled files in sdist (thanks @koobs).
- Fix Tornado support behavior for Tornado 3 (thanks @abhinav).
- decode_compressed_response option and filter (thanks @jayvdb).
- 1.7.4 [#217]
- Make use_cassette decorated functions actually return a value (thanks @bcen).
- [#199] Fix path transformation defaults.
- Better headers dictionary management.
- 1.7.3 [#188]
- ``additional_matchers`` kwarg on ``use_cassette``.
- [#191] Actually support passing multiple before_record_request functions (thanks @agriffis).
- 1.7.2
- [#186] Get effective_url in tornado (thanks @mvschaik)
- [#187] Set request_time on Response object in tornado (thanks @abhinav).
- 1.7.1
- [#183] Patch ``fetch_impl`` instead of the entire HTTPClient class for Tornado (thanks @abhinav).
- 1.7.0
- [#177] Properly support coroutine/generator decoration.
- [#178] Support distribute (thanks @graingert). [#163] Make compatibility between python2 and python3 recorded cassettes more robust (thanks @gward).
- 1.6.1
- [#169] Support conditional requirements in old versions of pip
- Fix RST parse errors generated by pandoc
- [Tornado] Fix unsupported features exception not being raised
- [#166] content-aware body matcher.
- 1.6.0
- [#120] Tornado support (thanks @abhinav)
- [#147] packaging fixes (thanks @graingert)
- [#158] allow filtering post params in requests (thanks @MrJohz)
- [#140] add xmlrpclib support (thanks @Diaoul).
- 1.5.2
- Fix crash when cassette path contains cassette library directory (thanks @gazpachoking).
- 1.5.0
- Automatic cassette naming and 'application/json' post data filtering (thanks @marco-santamaria).
- 1.4.2
- Fix a bug caused by requests 2.7 and chunked transfer encoding
- 1.4.1
- Include README, tests, LICENSE in package. Thanks @ralphbean.
- 1.4.0
- Filter post data parameters (thanks @eadmundo)
- Support for posting files through requests, inject\_cassette kwarg to access cassette from ``use_cassette`` decorated function, ``with_current_defaults`` actually works (thanks @samstav).
- 1.3.0
- Fix/add support for urllib3 (thanks @aisch)
- Fix default port for https (thanks @abhinav).
- 1.2.0
- Add custom\_patches argument to VCR/Cassette objects to allow users to stub custom classes when cassettes become active.
- 1.1.4
- Add force reset around calls to actual connection from stubs, to ensure compatibility with the version of httplib/urlib2 in python 2.7.9.
- 1.1.3
- Fix python3 headers field (thanks @rtaboada)
- fix boto test (thanks @telaviv)
- fix new\_episodes record mode (thanks @jashugan),
- fix Windows connectionpool stub bug (thanks @gazpachoking)
- add support for requests 2.5
- 1.1.2
- Add urllib==1.7.1 support.
- Make json serialize error handling correct
- Improve logging of match failures.
- 1.1.1
- Use function signature preserving ``wrapt.decorator`` to write the decorator version of use\_cassette in order to ensure compatibility with py.test fixtures and python 2.
- Move all request filtering into the ``before_record_callable``.
- 1.1.0
- Add ``before_record_response``. Fix several bugs related to the context management of cassettes.
- 1.0.3
- Fix an issue with requests 2.4 and make sure case sensitivity is consistent across python versions
- 1.0.2
- Fix an issue with requests 2.3
- 1.0.1
- Fix a bug with the new ignore requests feature and the once record mode
- 1.0.0
- *BACKWARDS INCOMPATIBLE*: Please see the 'upgrade' section in the README. Take a look at the matcher section as well, you might want to update your ``match_on`` settings.
- Add support for filtering sensitive data from requests, matching query strings after the order changes and improving the built-in matchers, (thanks to @mshytikov)
- Support for ignoring requests to certain hosts, bump supported Python3 version to 3.4, fix some bugs with Boto support (thanks @marusich)
- Fix error with URL field capitalization in README (thanks @simon-weber)
- Added some log messages to help with debugging
- Added ``all_played`` property on cassette (thanks @mshytikov)
- 0.7.0
- VCR.py now supports Python 3! (thanks @asundg)
- Also I refactored the stub connections quite a bit to add support for the putrequest and putheader calls.
- This version also adds support for httplib2 (thanks @nilp0inter).
- I have added a couple tests for boto since it is an http client in its own right.
- Finally, this version includes a fix for a bug where requests wasn't being patched properly (thanks @msabramo).
- 0.6.0
- Store response headers as a list since a HTTP response can have the same header twice (happens with set-cookie sometimes).
- This has the added benefit of preserving the order of headers.
- Thanks @smallcode for the bug report leading to this change.
- I have made an effort to ensure backwards compatibility with the old cassettes' header storage mechanism, but if you want to upgrade to the new header storage, you should delete your cassettes and re-record them.
- Also this release adds better error messages (thanks @msabramo)
- and adds support for using VCR as a decorator (thanks @smallcode for the motivation)
- 0.5.0
- Change the ``response_of`` method to ``responses_of`` since cassettes can now contain more than one response for a request.
- Since this changes the API, I'm bumping the version.
- Also includes 2 bugfixes:
- a better error message when attempting to overwrite a cassette file,
- and a fix for a bug with requests sessions (thanks @msabramo)
- 0.4.0
- Change default request recording behavior for multiple requests.
- If you make the same request multiple times to the same URL, the response might be different each time (maybe the response has a timestamp in it or something), so this will make the same request multiple times and save them all.
- Then, when you are replaying the cassette, the responses will be played back in the same order in which they were received.
- If you were making multiple requests to the same URL in a cassette before version 0.4.0, you might need to regenerate your cassette files.
- Also, removes support for the cassette.play\_count counter API, since individual requests aren't unique anymore.
- A cassette might contain the same request several times.
- Also removes secure overwrite feature since that was breaking overwriting files in Windows
- And fixes a bug preventing request's automatic body decompression from working.
- 0.3.5
- Fix compatibility with requests 2.x
- 0.3.4
- Bugfix: close file before renaming it. This fixes an issue on Windows. Thanks @smallcode for the fix.
- 0.3.3
- Bugfix for error message when an unregistered custom matcher was used
- 0.3.2
- Fix issue with new config syntax and the ``match_on`` parameter. Thanks, @chromy!
- 0.3.1
- Fix issue causing full paths to be sent on the HTTP request line.
- 0.3.0
- *Backwards incompatible release*
- Added support for record modes, and changed the default recording behavior to the "once" record mode. Please see the documentation on record modes for more.
- Added support for custom request matching, and changed the default request matching behavior to match only on the URL and method.
- Also, improved the httplib mocking to add support for the ``HTTPConnection.send()`` method.
- This means that requests won't actually be sent until the response is read, since I need to record the entire request in order to match up the appropriate response.
- I don't think this should cause any issues unless you are sending requests without ever loading the response (which none of the standard httplib wrappers do, as far as I know).
- Thanks to @fatuhoku for some of the ideas and the motivation behind this release.
- 0.2.1
- Fixed missing modules in setup.py
- 0.2.0
- Added configuration API, which lets you configure some settings on VCR (see the README).
- Also, VCR no longer saves cassettes if they haven't changed at all and supports JSON as well as YAML (thanks @sirpengi).
- Added amazing new skeumorphic logo, thanks @hairarrow.
- 0.1.0
- *backwards incompatible release - delete your old cassette files*
- This release adds the ability to access the cassette to make assertions on it
- as well as a major code refactor thanks to @dlecocq.
- It also fixes a couple longstanding bugs with redirects and HTTPS. [#3 and #4]
- 0.0.4
- If you have libyaml installed, vcrpy will use the c bindings instead. Speed up your tests! Thanks @dlecocq
- 0.0.3
- Add support for requests 1.2.3. Support for older versions of requests dropped (thanks @vitormazzi and @bryanhelmig)
- 0.0.2
- Add support for requests / urllib3
- 0.0.1
- Initial Release

View File

@@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
#
# vcrpy documentation build configuration file, created by
# sphinx-quickstart on Sun Sep 13 11:18:00 2015.
@@ -12,8 +11,33 @@
# All configuration values have a default; values that are commented out
# serve to show the default.
import codecs
import os
import re
here = os.path.abspath(os.path.dirname(__file__))
def read(*parts):
# intentionally *not* adding an encoding option to open, See:
# https://github.com/pypa/virtualenv/issues/201#issuecomment-3145690
with codecs.open(os.path.join(here, *parts), "r") as fp:
return fp.read()
def find_version(*file_paths):
version_file = read(*file_paths)
version_match = re.search(r"^__version__ = ['\"]([^'\"]*)['\"]", version_file, re.M)
if version_match:
return version_match.group(1)
raise RuntimeError("Unable to find version string.")
autodoc_default_options = {
"members": None,
"undoc-members": None,
}
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
@@ -27,7 +51,14 @@ import os
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = ["sphinx.ext.autodoc", "sphinx.ext.intersphinx", "sphinx.ext.coverage", "sphinx.ext.viewcode"]
extensions = [
"sphinx.ext.autodoc",
"sphinx.ext.intersphinx",
"sphinx.ext.coverage",
"sphinx.ext.viewcode",
"sphinx.ext.todo",
"sphinx.ext.githubpages",
]
# Add any paths that contain templates here, relative to this directory.
templates_path = ["_templates"]
@@ -44,25 +75,25 @@ source_suffix = ".rst"
master_doc = "index"
# General information about the project.
project = u"vcrpy"
copyright = u"2015, Kevin McCarthy"
author = u"Kevin McCarthy"
project = "vcrpy"
copyright = "2015, Kevin McCarthy"
author = "Kevin McCarthy"
# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
# built documents.
#
# The short X.Y version.
version = "1.7.4"
# version = "1.7.4"
# The full version, including alpha/beta/rc tags.
release = "1.7.4"
version = release = find_version("..", "vcr", "__init__.py")
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
#
# 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:
@@ -106,11 +137,11 @@ todo_include_todos = False
# The theme to use for HTML and HTML Help pages.
# https://read-the-docs.readthedocs.io/en/latest/theme.html#how-do-i-use-this-locally-and-on-read-the-docs
if "READTHEDOCS" not in os.environ:
import sphinx_rtd_theme
html_theme = "sphinx_rtd_theme"
html_theme_path = [sphinx_rtd_theme.get_html_theme_path()]
# if "READTHEDOCS" not in os.environ:
# import sphinx_rtd_theme
#
# html_theme = "sphinx_rtd_theme"
# html_theme_path = [sphinx_rtd_theme.get_html_theme_path()]
# Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the
@@ -153,6 +184,8 @@ html_static_path = ["_static"]
# Custom sidebar templates, maps document names to template names.
# html_sidebars = {}
html_sidebars = {"**": ["globaltoc.html", "relations.html", "sourcelink.html", "searchbox.html"]}
# Additional templates that should be rendered to pages, maps page names to
# template names.
@@ -205,19 +238,21 @@ htmlhelp_basename = "vcrpydoc"
latex_elements = {
# The paper size ('letterpaper' or 'a4paper').
#'papersize': 'letterpaper',
# 'papersize': 'letterpaper',
# The font size ('10pt', '11pt' or '12pt').
#'pointsize': '10pt',
# 'pointsize': '10pt',
# Additional stuff for the LaTeX preamble.
#'preamble': '',
# 'preamble': '',
# Latex figure (float) alignment
#'figure_align': 'htbp',
# 'figure_align': 'htbp',
}
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title,
# author, documentclass [howto, manual, or own class]).
latex_documents = [(master_doc, "vcrpy.tex", u"vcrpy Documentation", u"Kevin McCarthy", "manual")]
latex_documents = [
(master_doc, "vcrpy.tex", "vcrpy Documentation", "Kevin McCarthy", "manual"),
]
# The name of an image file (relative to this directory) to place at the top of
# the title page.
@@ -244,7 +279,7 @@ latex_documents = [(master_doc, "vcrpy.tex", u"vcrpy Documentation", u"Kevin McC
# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [(master_doc, "vcrpy", u"vcrpy Documentation", [author], 1)]
man_pages = [(master_doc, "vcrpy", "vcrpy Documentation", [author], 1)]
# If true, show URL addresses after external links.
# man_show_urls = False
@@ -259,12 +294,12 @@ texinfo_documents = [
(
master_doc,
"vcrpy",
u"vcrpy Documentation",
"vcrpy Documentation",
author,
"vcrpy",
"One line description of project.",
"Miscellaneous",
)
),
]
# Documents to append as an appendix to all manuals.
@@ -282,3 +317,4 @@ texinfo_documents = [
# Example configuration for intersphinx: refer to the Python standard library.
intersphinx_mapping = {"https://docs.python.org/": None}
html_theme = "alabaster"

View File

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

View File

@@ -1,23 +1,106 @@
Contributing
============
.. image:: _static/vcr.svg
:alt: vcr.py logo
:align: right
🚀 Milestones
--------------
For anyone interested in the roadmap and projected release milestones please see the following link:
`MILESTONES <https://github.com/kevin1024/vcrpy/milestones>`_
----
🎁 Contributing Issues and PRs
-------------------------------
- Issues and PRs will get triaged and assigned to the appropriate milestone.
- PRs get priority over issues.
- The maintainers have limited bandwidth and do so **voluntarily**.
So whilst reporting issues are valuable, please consider:
- contributing an issue with a toy repo that replicates the issue.
- contributing PRs is a more valuable donation of your time and effort.
Thanks again for your interest and support in VCRpy.
We really appreciate it.
----
👥 Collaborators
-----------------
We also have a large test matrix to cover and would like members to volunteer covering these roles.
============ ==================== ================= ================== ======================
**Library** **Issue Triager(s)** **Maintainer(s)** **PR Reviewer(s)** **Release Manager(s)**
------------ -------------------- ----------------- ------------------ ----------------------
``core`` Needs support Needs support Needs support @neozenith
``requests`` @neozenith Needs support @neozenith @neozenith
``aiohttp`` Needs support Needs support Needs support @neozenith
``urllib3`` Needs support Needs support Needs support @neozenith
``httplib2`` Needs support Needs support Needs support @neozenith
``tornado4`` Needs support Needs support Needs support @neozenith
``boto3`` Needs support Needs support Needs support @neozenith
============ ==================== ================= ================== ======================
Role Descriptions
~~~~~~~~~~~~~~~~~
**Issue Triager:**
Simply adding these three labels for incoming issues means a lot for maintaining this project:
- ``bug`` or ``enhancement``
- Which library does it affect? ``core``, ``aiohttp``, ``requests``, ``urllib3``, ``tornado4``, ``httplib2``
- If it is a bug, is it ``Verified Can Replicate`` or ``Requires Help Replicating``
- Thanking people for raising issues. Feedback is always appreciated.
- Politely asking if they are able to link to an example repo that replicates the issue if they haven't already. Being able to *clone and go* helps the next person and we like that. 😃
**Maintainer:**
This involves creating PRs to address bugs and enhancement requests. It also means maintaining the test suite, docstrings and documentation .
**PR Reviewer:**
The PR reviewer is a second set of eyes to see if:
- Are there tests covering the code paths added/modified?
- Do the tests and modifications make sense seem appropriate?
- Add specific feedback, even on approvals, why it is accepted. eg "I like how you use a context manager there. 😄 "
- Also make sure they add a line to `docs/changelog.rst` to claim credit for their contribution.
**Release Manager:**
- Ensure CI is passing.
- Create a release on github and tag it with the changelog release notes.
- ``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/
----
Running VCR's test suite
------------------------
The tests are all run automatically on `Travis
CI <https://travis-ci.org/kevin1024/vcrpy>`__, but you can also run them
yourself using `py.test <http://pytest.org/>`__ and
`Tox <http://tox.testrun.org/>`__. Tox will automatically run them in
all environments VCR.py supports. The test suite is pretty big and slow,
but you can tell tox to only run specific tests like this::
yourself using `pytest <http://pytest.org/>`__ and
`Tox <http://tox.testrun.org/>`__.
Tox will automatically run them in all environments VCR.py supports if they are available on your `PATH`. Alternatively you can use `tox-pyenv <https://pypi.org/project/tox-pyenv/>`_ with
`pyenv <https://github.com/pyenv/pyenv>`_.
We recommend you read the documentation for each and see the section further below.
The test suite is pretty big and slow, but you can tell tox to only run specific tests like this::
tox -e {pyNN}-{HTTP_LIBRARY} -- <pytest flags passed through>
tox -e py27-requests -- -v -k "'test_status_code or test_gzip'"
tox -e py37-requests -- -v --last-failed
tox -e py38-requests -- -v -k "'test_status_code or test_gzip'"
tox -e py38-requests -- -v --last-failed
This will run only tests that look like ``test_status_code`` or
``test_gzip`` in the test suite, and only in the python 2.7 environment
``test_gzip`` in the test suite, and only in the python 3.8 environment
that has ``requests`` installed.
Also, in order for the boto tests to run, you will need an AWS key.
@@ -31,8 +114,8 @@ Using PyEnv with VCR's test suite
---------------------------------
PyEnv is a tool for managing multiple installation of python on your system.
See the full documentation at their `github <https://github.com/pyenv/pyenv>`
but we are also going to use `tox-pyenv <https://pypi.org/project/tox-pyenv/>`
See the full documentation at their `github <https://github.com/pyenv/pyenv>`_
but we are also going to use `tox-pyenv <https://pypi.org/project/tox-pyenv/>`_
in this example::
git clone https://github.com/pyenv/pyenv ~/.pyenv
@@ -44,20 +127,20 @@ 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 2.7.10 3.5.7 3.6.9 3.7.4 3.8-dev pypy2.6-7.1.1 pypy3.6-7.1.1
pyenv install 3.8.0 pypy3.8
# This activates them
pyenv local 2.7.10 3.5.7 3.6.9 3.7.4 3.8-dev pypy2.6-7.1.1 pypy3.6-7.1.1
pyenv local 3.8.0 pypy3.8
# Run the whole test suite
tox
# Run the whole test suite or just part of it
tox -e lint
tox -e py37-requests
tox -e py38-requests
Troubleshooting on MacOSX

View File

@@ -31,7 +31,7 @@ which matchers didn't match. This can help you with debugging custom
matchers.
CannotOverwriteExistingCassetteException
========================================
----------------------------------------
When a request failed to be found in an existing cassette,
VCR.py tries to get the request(s) that may be similar to the one being searched.
@@ -44,7 +44,7 @@ It can return multiple similar requests with :
CannotOverwriteExistingCassetteException message example :
.. code:: python
.. code::
CannotOverwriteExistingCassetteException: Can't overwrite existing cassette ('cassette.yaml') in your current record mode ('once').
No match for the request (<Request (GET) https://www.googleapis.com/?alt=json&maxResults=200>) was found.
@@ -54,4 +54,4 @@ CannotOverwriteExistingCassetteException message example :
Matchers succeeded : ['method', 'scheme', 'host', 'port', 'path']
Matchers failed :
query - assertion failure :
[('alt', 'json'), ('maxResults', '200')] != [('alt', 'json'), ('maxResults', '500')]
[('alt', 'json'), ('maxResults', '200')] != [('alt', 'json'), ('maxResults', '500')]

View File

@@ -4,7 +4,7 @@ Contents
========
.. toctree::
:maxdepth: 2
:maxdepth: 3
installation
usage
@@ -15,6 +15,7 @@ Contents
contributing
changelog
==================
Indices and tables
==================

View File

@@ -4,13 +4,12 @@ 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
-------------
VCR.py supports Python 2.7 and 3.5+, and
`pypy <http://pypy.org>`__.
VCR.py supports Python 3.8+, and `pypy <http://pypy.org>`__.
The following HTTP libraries are supported:
@@ -23,6 +22,7 @@ The following HTTP libraries are supported:
- ``tornado.httpclient``
- ``urllib2``
- ``urllib3``
- ``httpx``
Speed
-----
@@ -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 urllib2
import urllib.request
with vcr.use_cassette('fixtures/vcr_cassettes/synopsis.yaml'):
response = urllib2.urlopen('http://www.iana.org/domains/reserved').read()
assert 'Example domains' in response
response = urllib.request.urlopen('http://www.iana.org/domains/reserved').read()
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
@@ -25,8 +25,8 @@ look like this:
@vcr.use_cassette('fixtures/vcr_cassettes/synopsis.yaml')
def test_iana():
response = urllib2.urlopen('http://www.iana.org/domains/reserved').read()
assert 'Example domains' in response
response = urllib.request.urlopen('http://www.iana.org/domains/reserved').read()
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.
@@ -35,8 +35,8 @@ omit the path to the cassette file.
@vcr.use_cassette()
def test_iana():
response = urllib2.urlopen('http://www.iana.org/domains/reserved').read()
assert 'Example domains' in response
response = urllib.request.urlopen('http://www.iana.org/domains/reserved').read()
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` py.test $*
REQUESTS_CA_BUNDLE=`python3 -m pytest_httpbin.certs` exec pytest "$@"

View File

@@ -1,11 +1,31 @@
#!/usr/bin/env python
import codecs
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()
long_description = open("README.rst").read()
here = os.path.abspath(os.path.dirname(__file__))
def read(*parts):
# intentionally *not* adding an encoding option to open, See:
# https://github.com/pypa/virtualenv/issues/201#issuecomment-3145690
with codecs.open(os.path.join(here, *parts), "r") as fp:
return fp.read()
def find_version(*file_paths):
version_file = read(*file_paths)
version_match = re.search(r"^__version__ = ['\"]([^'\"]*)['\"]", version_file, re.M)
if version_match:
return version_match.group(1)
raise RuntimeError("Unable to find version string.")
class PyTest(TestCommand):
@@ -26,40 +46,60 @@ install_requires = [
"PyYAML",
"wrapt",
"six>=1.5",
'contextlib2; python_version=="2.7"',
'mock; python_version=="2.7"',
'yarl; python_version>="3.6"',
'yarl<1.4; python_version=="3.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'",
]
excluded_packages = ["tests*"]
if sys.version_info[0] == 2:
excluded_packages.append("vcr.stubs.aiohttp_stubs")
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(
name="vcrpy",
version="3.0.0",
description=("Automatically mock your HTTP interactions to simplify and " "speed up testing"),
version=find_version("vcr", "__init__.py"),
description=("Automatically mock your HTTP interactions to simplify and speed up testing"),
long_description=long_description,
long_description_content_type="text/x-rst",
author="Kevin McCarthy",
author_email="me@kevinmccarthy.org",
url="https://github.com/kevin1024/vcrpy",
packages=find_packages(exclude=excluded_packages),
python_requires=">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*",
packages=find_packages(exclude=["tests*"]),
python_requires=">=3.8",
install_requires=install_requires,
license="MIT",
tests_require=["pytest", "mock", "pytest-httpbin"],
tests_require=tests_require,
classifiers=[
"Development Status :: 5 - Production/Stable",
"Environment :: Console",
"Intended Audience :: Developers",
"Programming Language :: Python",
"Programming Language :: Python :: 2",
"Programming Language :: Python :: 2.7",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.5",
"Programming Language :: Python :: 3.6",
"Programming Language :: Python :: 3.7",
"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",
"Topic :: Software Development :: Testing",

View File

@@ -11,9 +11,10 @@ def assert_cassette_has_one_response(cass):
assert cass.play_count == 1
def assert_is_json(a_string):
def assert_is_json_bytes(b: bytes):
assert isinstance(b, bytes)
try:
json.loads(a_string.decode("utf-8"))
json.loads(b.decode("utf-8"))
except Exception:
assert False
assert True

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

@@ -0,0 +1,41 @@
interactions:
- request:
body: ''
headers:
accept:
- '*/*'
accept-encoding:
- gzip, deflate, br
connection:
- keep-alive
host:
- httpbin.org
user-agent:
- python-httpx/0.12.1
method: GET
uri: https://mockbin.org/headers
response:
content: "{\n \"headers\": {\n \"Accept\": \"*/*\", \n \"Accept-Encoding\"\
: \"gzip, deflate, br\", \n \"Host\": \"httpbin.org\", \n \"User-Agent\"\
: \"python-httpx/0.12.1\", \n \"X-Amzn-Trace-Id\": \"Root=1-5ea778c9-ea76170da792abdbf7614067\"\
\n }\n}\n"
headers:
access-control-allow-credentials:
- 'true'
access-control-allow-origin:
- '*'
connection:
- keep-alive
content-length:
- '226'
content-type:
- application/json
date:
- Tue, 28 Apr 2020 00:28:57 GMT
server:
- gunicorn/19.9.0
via:
- my_own_proxy
http_version: HTTP/1.1
status_code: 200
version: 1

View File

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

@@ -1,5 +1,6 @@
import contextlib
import logging
import urllib.parse
import pytest
@@ -7,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
@@ -32,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)
@@ -49,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)
@@ -66,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)
@@ -78,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)
@@ -89,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"))):
@@ -102,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")
@@ -113,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})
@@ -144,19 +151,21 @@ def test_post(tmpdir, scheme, body, caplog):
(
log
for log in caplog.records
if log.getMessage() == "<Request (POST) {}> not in cassette, sending to real server".format(url)
if log.getMessage() == f"<Request (POST) {url}> not in cassette, sending to real server"
),
None,
), "Log message not found."
def test_params(tmpdir, scheme):
url = scheme + "://httpbin.org/get"
@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": False, "c": "c"}
params = {"a": 1, "b": 2, "c": "c"}
with vcr.use_cassette(str(tmpdir.join("get.yaml"))) as cassette:
_, response_json = get(url, output="json", params=params, headers=headers)
assert response_json["queryString"] == {"a": "1", "b": "2", "c": "c", "d": "d"}
with vcr.use_cassette(str(tmpdir.join("get.yaml"))) as cassette:
_, cassette_response_json = get(url, output="json", params=params, headers=headers)
@@ -164,10 +173,11 @@ 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": False, "c": "c"}
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)
@@ -179,13 +189,13 @@ def test_params_same_url_distinct_params(tmpdir, scheme):
other_params = {"other": "params"}
with vcr.use_cassette(str(tmpdir.join("get.yaml"))) as cassette:
response, cassette_response_text = get(url, output="text", params=other_params)
assert "No match for the request" in cassette_response_text
assert response.status == 599
with pytest.raises(vcr.errors.CannotOverwriteExistingCassetteException):
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:
@@ -249,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://httpbin.org/redirect/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)
@@ -273,13 +284,32 @@ def test_redirect(aiohttp_client, tmpdir):
assert cassette_response.request_info.real_url == response.request_info.real_url
def test_double_requests(tmpdir):
"""We should capture, record, and replay all requests and response chains,
even if there are duplicate ones.
@pytest.mark.online
def test_not_modified(tmpdir, mockbin):
"""It doesn't try to redirect on 304"""
url = mockbin + "/status/304"
We should replay in the order we saw them.
with vcr.use_cassette(str(tmpdir.join("not_modified.yaml"))):
response, _ = get(url)
with vcr.use_cassette(str(tmpdir.join("not_modified.yaml"))) as cassette:
cassette_response, _ = get(url)
assert cassette_response.status == 304
assert response.status == 304
assert len(cassette_response.history) == len(response.history)
assert len(cassette) == 1
assert cassette.play_count == 1
@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")
@@ -302,3 +332,104 @@ def test_double_requests(tmpdir):
# Now that we made both requests, we should have played both.
assert cassette.play_count == 2
def test_cookies(httpbin_both, httpbin_ssl_context, tmpdir):
async def run(loop):
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 = 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, 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, 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)
def assert_responses(cookies_resp, home_resp):
assert cookies_resp.cookies.get("cookie_1").value == "val_1"
assert cookies_resp.cookies.get("Cookie_2").value == "Val_2"
request_cookies = home_resp.request_info.headers["cookie"]
assert "cookie_1=val_1" in request_cookies
assert "Cookie_2=Val_2" in request_cookies
assert "Cookie_3=Val_3" in request_cookies
assert "Cookie_4=Val_4" in request_cookies
run_in_loop(run)
def test_cookies_redirect(httpbin_both, httpbin_ssl_context, tmpdir):
async def run(loop):
# Sets cookie as provided by the query string and redirects
cookies_url = httpbin_both.url + "/cookies/set?Cookie_1=Val_1"
tmp = str(tmpdir.join("cookies.yaml"))
# ------------------------- Record -------------------------- #
with vcr.use_cassette(tmp) as cassette:
async with aiohttp.ClientSession(loop=loop, cookie_jar=aiohttp.CookieJar(unsafe=True)) as session:
cookies_resp = await session.get(cookies_url, ssl=httpbin_ssl_context)
assert not cookies_resp.cookies
cookies = session.cookie_jar.filter_cookies(cookies_url)
assert cookies["Cookie_1"].value == "Val_1"
assert cassette.play_count == 0
cassette.requests[1].headers["Cookie"] == "Cookie_1=Val_1"
# -------------------------- Play --------------------------- #
with vcr.use_cassette(tmp, record_mode=vcr.mode.NONE) as cassette:
async with aiohttp.ClientSession(loop=loop, 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"
assert cassette.play_count == 2
cassette.requests[1].headers["Cookie"] == "Cookie_1=Val_1"
# Assert that it's ignoring expiration date
with vcr.use_cassette(tmp, record_mode=vcr.mode.NONE) as cassette:
cassette.responses[0]["headers"]["set-cookie"] = [
"Cookie_1=Val_1; Expires=Wed, 21 Oct 2015 07:28:00 GMT"
]
async with aiohttp.ClientSession(loop=loop, 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"
run_in_loop(run)
@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):
response, _ = get(url, allow_redirects=False)
assert response.url.path == "/redirect/308/5"
assert response.status == 308
with vcr.use_cassette(path) as cassette:
response, _ = get(url, allow_redirects=False)
assert response.url.path == "/redirect/308/5"
assert response.status == 308
assert cassette.play_count == 1

View File

@@ -1,9 +1,8 @@
# -*- coding: utf-8 -*-
"""Basic tests for cassettes"""
# External imports
import os
from six.moves.urllib.request import urlopen
from urllib.request import urlopen
# Internal imports
import vcr
@@ -11,7 +10,7 @@ import vcr
def test_nonexistent_directory(tmpdir, httpbin):
"""If we load a cassette in a nonexistent directory, it can save ok"""
# Check to make sure directory doesnt exist
# Check to make sure directory doesn't exist
assert not os.path.exists(str(tmpdir.join("nonexistent")))
# Run VCR to create dir and cassette file

View File

@@ -2,17 +2,14 @@ import pytest
boto = pytest.importorskip("boto")
from configparser import DuplicateSectionError # NOQA
import boto # NOQA
import boto.iam # NOQA
from boto.s3.connection import S3Connection # NOQA
from boto.s3.key import Key # NOQA
import vcr # NOQA
try: # NOQA
from ConfigParser import DuplicateSectionError # NOQA
except ImportError: # NOQA
# python3
from configparser import DuplicateSectionError # NOQA
import vcr # NOQA
def test_boto_stubs(tmpdir):
@@ -20,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:
@@ -18,12 +20,12 @@ except ImportError:
# https://github.com/boto/botocore/pull/1495
boto3_skip_vendored_requests = pytest.mark.skipif(
botocore_awsrequest,
reason="botocore version {ver} does not use vendored requests anymore.".format(ver=botocore.__version__),
reason=f"botocore version {botocore.__version__} does not use vendored requests anymore.",
)
boto3_skip_awsrequest = pytest.mark.skipif(
not botocore_awsrequest,
reason="botocore version {ver} still uses vendored requests.".format(ver=botocore.__version__),
reason=f"botocore version {botocore.__version__} still uses vendored requests.",
)
IAM_USER_NAME = "vcrpy"
@@ -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,50 +1,58 @@
import os
import json
import os
from urllib.request import urlopen
import pytest
import vcr
from six.moves.urllib.request import urlopen
def test_set_serializer_default_config(tmpdir, httpbin):
@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:
assert json.loads(f.read())
file_content = f.read()
assert file_content.endswith("\n")
assert json.loads(file_content)
def test_default_set_cassette_library_dir(tmpdir, httpbin):
@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
@@ -56,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

@@ -1,28 +1,30 @@
# -*- coding: utf-8 -*-
"""Basic tests about save behavior"""
# External imports
import os
import time
from six.moves.urllib.request import urlopen
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 +32,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)
@@ -45,9 +48,9 @@ def test_disk_saver_write(tmpdir, httpbin):
# the mtime doesn't change
time.sleep(1)
with vcr.use_cassette(fname, record_mode="any") as cass:
urlopen(httpbin.url).read()
urlopen(httpbin.url + "/get").read()
with vcr.use_cassette(fname, record_mode=vcr.mode.ANY) as cass:
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,11 +1,13 @@
import base64
import pytest
from six.moves.urllib.request import urlopen, Request
from six.moves.urllib.parse import urlencode
from six.moves.urllib.error import HTTPError
import vcr
import json
from assertions import assert_cassette_has_one_response, assert_is_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_bytes
import vcr
def _request_with_auth(url, username, password):
@@ -103,7 +105,19 @@ def test_decompress_gzip(tmpdir, httpbin):
with vcr.use_cassette(cass_file) as cass:
decoded_response = urlopen(url).read()
assert_cassette_has_one_response(cass)
assert_is_json(decoded_response)
assert_is_json_bytes(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):
@@ -115,7 +129,7 @@ def test_decompress_deflate(tmpdir, httpbin):
with vcr.use_cassette(cass_file) as cass:
decoded_response = urlopen(url).read()
assert_cassette_has_one_response(cass)
assert_is_json(decoded_response)
assert_is_json_bytes(decoded_response)
def test_decompress_regular(tmpdir, httpbin):
@@ -127,4 +141,25 @@ def test_decompress_regular(tmpdir, httpbin):
with vcr.use_cassette(cass_file) as cass:
resp = urlopen(url).read()
assert_cassette_has_one_response(cass)
assert_is_json(resp)
assert_is_json_bytes(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,12 @@
# -*- coding: utf-8 -*-
"""Integration tests with httplib2"""
from urllib.parse import urlencode
import sys
from six.moves.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 +16,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,13 +55,15 @@ def test_response_headers(tmpdir, httpbin_both):
assert set(headers) == set(resp.items())
def test_effective_url(tmpdir, httpbin_both):
@pytest.mark.online
def test_effective_url(tmpdir):
"""Ensure that the effective_url is captured"""
url = httpbin_both.url + "/redirect-to?url=/html"
url = "http://mockbin.org/redirect/301"
with vcr.use_cassette(str(tmpdir.join("headers.yaml"))):
resp, _ = http().request(url)
effective_url = resp["content-location"]
assert effective_url == httpbin_both + "/html"
assert effective_url == "http://mockbin.org/redirect/301/0"
with vcr.use_cassette(str(tmpdir.join("headers.yaml"))):
resp, _ = http().request(url)
@@ -111,7 +107,7 @@ def test_post_data(tmpdir, httpbin_both):
def test_post_unicode_data(tmpdir, httpbin_both):
"""Ensure that it works when posting unicode data"""
data = urlencode({"snowman": u"".encode("utf-8")})
data = urlencode({"snowman": "".encode()})
url = httpbin_both.url + "/post"
with vcr.use_cassette(str(tmpdir.join("post_data.yaml"))):
_, res1 = http().request(url, "POST", data)

View File

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

View File

@@ -1,6 +1,7 @@
from six.moves.urllib.request import urlopen
import socket
from contextlib import contextmanager
from urllib.request import urlopen
import vcr
@@ -27,9 +28,9 @@ def test_ignore_localhost(tmpdir, httpbin):
with overridden_dns({"httpbin.org": "127.0.0.1"}):
cass_file = str(tmpdir.join("filter_qs.yaml"))
with vcr.use_cassette(cass_file, ignore_localhost=True) as cass:
urlopen("http://localhost:{}/".format(httpbin.port))
urlopen(f"http://localhost:{httpbin.port}/")
assert len(cass) == 0
urlopen("http://httpbin.org:{}/".format(httpbin.port))
urlopen(f"http://httpbin.org:{httpbin.port}/")
assert len(cass) == 1
@@ -37,9 +38,9 @@ def test_ignore_httpbin(tmpdir, httpbin):
with overridden_dns({"httpbin.org": "127.0.0.1"}):
cass_file = str(tmpdir.join("filter_qs.yaml"))
with vcr.use_cassette(cass_file, ignore_hosts=["httpbin.org"]) as cass:
urlopen("http://httpbin.org:{}/".format(httpbin.port))
urlopen(f"http://httpbin.org:{httpbin.port}/")
assert len(cass) == 0
urlopen("http://localhost:{}/".format(httpbin.port))
urlopen(f"http://localhost:{httpbin.port}/")
assert len(cass) == 1
@@ -47,8 +48,8 @@ def test_ignore_localhost_and_httpbin(tmpdir, httpbin):
with overridden_dns({"httpbin.org": "127.0.0.1"}):
cass_file = str(tmpdir.join("filter_qs.yaml"))
with vcr.use_cassette(cass_file, ignore_hosts=["httpbin.org"], ignore_localhost=True) as cass:
urlopen("http://httpbin.org:{}".format(httpbin.port))
urlopen("http://localhost:{}".format(httpbin.port))
urlopen(f"http://httpbin.org:{httpbin.port}")
urlopen(f"http://localhost:{httpbin.port}")
assert len(cass) == 0
@@ -56,12 +57,12 @@ def test_ignore_localhost_twice(tmpdir, httpbin):
with overridden_dns({"httpbin.org": "127.0.0.1"}):
cass_file = str(tmpdir.join("filter_qs.yaml"))
with vcr.use_cassette(cass_file, ignore_localhost=True) as cass:
urlopen("http://localhost:{}".format(httpbin.port))
urlopen(f"http://localhost:{httpbin.port}")
assert len(cass) == 0
urlopen("http://httpbin.org:{}".format(httpbin.port))
urlopen(f"http://httpbin.org:{httpbin.port}")
assert len(cass) == 1
with vcr.use_cassette(cass_file, ignore_localhost=True) as cass:
assert len(cass) == 1
urlopen("http://localhost:{}".format(httpbin.port))
urlopen("http://httpbin.org:{}".format(httpbin.port))
urlopen(f"http://localhost:{httpbin.port}")
urlopen(f"http://httpbin.org:{httpbin.port}")
assert len(cass) == 1

View File

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

View File

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

View File

@@ -1,5 +1,8 @@
from urllib.request import urlopen
import pytest
import vcr
from six.moves.urllib.request import urlopen
def true_matcher(r1, r2):
@@ -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

@@ -1,18 +1,19 @@
# -*- coding: utf-8 -*-
"""Tests for cassettes with custom persistence"""
# External imports
import os
from six.moves.urllib.request import urlopen
from urllib.request import urlopen
import pytest
# Internal imports
import vcr
from vcr.persisters.filesystem import FilesystemPersister
from vcr.persisters.filesystem import CassetteDecodeError, CassetteNotFoundError, FilesystemPersister
class CustomFilesystemPersister(object):
class CustomFilesystemPersister:
"""Behaves just like default FilesystemPersister but adds .test extension
to the cassette file"""
to the cassette file"""
@staticmethod
def load_cassette(cassette_path, serializer):
@@ -25,12 +26,25 @@ class CustomFilesystemPersister(object):
FilesystemPersister.save_cassette(cassette_path, cassette_dict, serializer)
class BadPersister(FilesystemPersister):
"""A bad persister that raises different errors."""
@staticmethod
def load_cassette(cassette_path, serializer):
if "nonexistent" in cassette_path:
raise CassetteNotFoundError()
elif "encoding" in cassette_path:
raise CassetteDecodeError()
else:
raise ValueError("buggy persister")
def test_save_cassette_with_custom_persister(tmpdir, httpbin):
"""Ensure you can save a cassette using custom persister"""
my_vcr = vcr.VCR()
my_vcr.register_persister(CustomFilesystemPersister)
# Check to make sure directory doesnt exist
# Check to make sure directory doesn't exist
assert not os.path.exists(str(tmpdir.join("nonexistent")))
# Run VCR to create dir and cassette file using new save_cassette callback
@@ -53,3 +67,22 @@ def test_load_cassette_with_custom_persister(tmpdir, httpbin):
with my_vcr.use_cassette(test_fixture, serializer="json"):
response = urlopen(httpbin.url).read()
assert b"difficult sometimes" in response
def test_load_cassette_persister_exception_handling(tmpdir, httpbin):
"""
Ensure expected errors from persister are swallowed while unexpected ones
are passed up the call stack.
"""
my_vcr = vcr.VCR()
my_vcr.register_persister(BadPersister)
with my_vcr.use_cassette("bad/nonexistent") as cass:
assert len(cass) == 0
with my_vcr.use_cassette("bad/encoding") as cass:
assert len(cass) == 0
with pytest.raises(ValueError):
with my_vcr.use_cassette("bad/buggy") as cass:
pass

View File

@@ -1,7 +1,7 @@
import vcr
class MockSerializer(object):
class MockSerializer:
def __init__(self):
self.serialize_count = 0
self.deserialize_count = 0

View File

@@ -1,5 +1,6 @@
from urllib.request import urlopen
import vcr
from six.moves.urllib.request import urlopen
def test_recorded_request_uri_with_redirected_request(tmpdir, httpbin):

View File

@@ -1,10 +1,8 @@
# -*- coding: utf-8 -*-
"""Test requests' interaction with vcr"""
import platform
import pytest
import sys
from assertions import assert_cassette_empty, assert_is_json_bytes
import vcr
from assertions import assert_cassette_empty, assert_is_json
requests = pytest.importorskip("requests")
from requests.exceptions import ConnectionError # noqa E402
@@ -116,26 +114,6 @@ def test_post_chunked_binary(tmpdir, httpbin):
assert req1 == req2
@pytest.mark.skipif("sys.version_info >= (3, 6)", strict=True, raises=ConnectionError)
@pytest.mark.skipif(
(3, 5) < sys.version_info < (3, 6) and platform.python_implementation() == "CPython",
reason="Fails on CPython 3.5",
)
def test_post_chunked_binary_secure(tmpdir, httpbin_secure):
"""Ensure that we can send chunked binary without breaking while trying to concatenate bytes with str."""
data1 = iter([b"data", b"to", b"send"])
data2 = iter([b"data", b"to", b"send"])
url = httpbin_secure.url + "/post"
with vcr.use_cassette(str(tmpdir.join("requests.yaml"))):
req1 = requests.post(url, data1).content
print(req1)
with vcr.use_cassette(str(tmpdir.join("requests.yaml"))):
req2 = requests.post(url, data2).content
assert req1 == req2
def test_redirects(tmpdir, httpbin_both):
"""Ensure that we can handle redirects"""
url = httpbin_both + "/redirect-to?url=bytes/1024"
@@ -150,6 +128,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
@@ -162,20 +151,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_bytes(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,9 +1,10 @@
import vcr
import zlib
import http.client as httplib
import json
import six.moves.http_client as httplib
import zlib
from assertions import assert_is_json
from assertions import assert_is_json_bytes
import vcr
def _headers_are_case_insensitive(host, port):
@@ -64,7 +65,7 @@ def test_original_decoded_response_is_not_modified(tmpdir, httpbin):
inside = conn.getresponse()
# Assert that we do not modify the original response while appending
# to the casssette.
# to the cassette.
assert "gzip" == inside.headers["content-encoding"]
# They should effectively be the same response.
@@ -83,7 +84,7 @@ def test_original_decoded_response_is_not_modified(tmpdir, httpbin):
inside = conn.getresponse()
assert "content-encoding" not in inside.headers
assert_is_json(inside.read())
assert_is_json_bytes(inside.read())
def _make_before_record_response(fields, replacement="[REDACTED]"):

View File

@@ -1,14 +1,13 @@
# -*- coding: utf-8 -*-
"""Test requests' interaction with vcr"""
import json
import pytest
from assertions import assert_cassette_empty, assert_is_json_bytes
import vcr
from vcr.errors import CannotOverwriteExistingCassetteException
from assertions import assert_cassette_empty, assert_is_json
tornado = pytest.importorskip("tornado")
http = pytest.importorskip("tornado.httpclient")
@@ -44,12 +43,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"""
@@ -90,10 +83,10 @@ def test_body(get_client, tmpdir, scheme):
@pytest.mark.gen_test
def test_effective_url(get_client, scheme, tmpdir):
"""Ensure that the effective_url is captured"""
url = scheme + "://httpbin.org/redirect-to?url=/html"
url = scheme + "://mockbin.org/redirect/301?url=/html"
with vcr.use_cassette(str(tmpdir.join("url.yaml"))):
effective_url = (yield get(get_client(), url)).effective_url
assert effective_url == scheme + "://httpbin.org/html"
assert effective_url == scheme + "://mockbin.org/redirect/301/0"
with vcr.use_cassette(str(tmpdir.join("url.yaml"))) as cass:
assert effective_url == (yield get(get_client(), url)).effective_url
@@ -156,7 +149,7 @@ def test_post(get_client, tmpdir, scheme):
@pytest.mark.gen_test
def test_redirects(get_client, tmpdir, scheme):
"""Ensure that we can handle redirects"""
url = scheme + "://httpbin.org/redirect-to?url=bytes/1024"
url = scheme + "://mockbin.org/redirect/301?url=bytes/1024"
with vcr.use_cassette(str(tmpdir.join("requests.yaml"))):
content = (yield get(get_client(), url)).body
@@ -201,11 +194,11 @@ def test_gzip(get_client, tmpdir, scheme):
with vcr.use_cassette(str(tmpdir.join("gzip.yaml"))):
response = yield get(get_client(), url, **kwargs)
assert_is_json(response.body)
assert_is_json_bytes(response.body)
with vcr.use_cassette(str(tmpdir.join("gzip.yaml"))) as cass:
response = yield get(get_client(), url, **kwargs)
assert_is_json(response.body)
assert_is_json_bytes(response.body)
assert 1 == cass.play_count

View File

@@ -1,16 +1,16 @@
# -*- coding: utf-8 -*-
"""Integration tests with urllib2"""
import ssl
from six.moves.urllib.request import urlopen
from six.moves.urllib_parse import urlencode
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,12 +56,14 @@ def test_response_headers(httpbin_both, tmpdir):
assert sorted(open1) == sorted(open2)
def test_effective_url(httpbin_both, tmpdir):
@mark.online
def test_effective_url(tmpdir):
"""Ensure that the effective_url is captured"""
url = httpbin_both.url + "/redirect-to?url=/html"
url = "http://mockbin.org/redirect/301"
with vcr.use_cassette(str(tmpdir.join("headers.yaml"))):
effective_url = urlopen_with_cafile(url).geturl()
assert effective_url == httpbin_both.url + "/html"
assert effective_url == "http://mockbin.org/redirect/301/0"
with vcr.use_cassette(str(tmpdir.join("headers.yaml"))):
assert effective_url == urlopen_with_cafile(url).geturl()
@@ -104,7 +106,7 @@ def test_post_data(httpbin_both, tmpdir):
def test_post_unicode_data(httpbin_both, tmpdir):
"""Ensure that it works when posting unicode data"""
data = urlencode({"snowman": u"".encode("utf-8")}).encode("utf-8")
data = urlencode({"snowman": "".encode()}).encode("utf-8")
url = httpbin_both.url + "/post"
with vcr.use_cassette(str(tmpdir.join("post_data.yaml"))):
res1 = urlopen_with_cafile(url, data).read()

View File

@@ -4,9 +4,11 @@
import pytest
import pytest_httpbin
from assertions import assert_cassette_empty, assert_is_json_bytes
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,9 +97,11 @@ def test_post(tmpdir, httpbin_both, verify_pool_mgr):
assert req1 == req2
def test_redirects(tmpdir, httpbin_both, verify_pool_mgr):
@pytest.mark.online
def test_redirects(tmpdir, verify_pool_mgr):
"""Ensure that we can handle redirects"""
url = httpbin_both.url + "/redirect-to?url=bytes/1024"
url = "http://mockbin.org/redirect/301"
with vcr.use_cassette(str(tmpdir.join("verify_pool_mgr.yaml"))):
content = verify_pool_mgr.request("GET", url).data
@@ -104,8 +109,9 @@ def test_redirects(tmpdir, httpbin_both, verify_pool_mgr):
assert content == verify_pool_mgr.request("GET", url).data
# Ensure that we've now cached *two* responses. One for the redirect
# and one for the final fetch
assert len(cass) == 2
assert cass.play_count == 2
assert len(cass) == 2
assert cass.play_count == 2
def test_cross_scheme(tmpdir, httpbin, httpbin_secure, verify_pool_mgr):
@@ -130,10 +136,10 @@ def test_gzip(tmpdir, httpbin_both, verify_pool_mgr):
with vcr.use_cassette(str(tmpdir.join("gzip.yaml"))):
response = verify_pool_mgr.request("GET", url)
assert_is_json(response.data)
assert_is_json_bytes(response.data)
with vcr.use_cassette(str(tmpdir.join("gzip.yaml"))):
assert_is_json(response.data)
assert_is_json_bytes(response.data)
def test_https_with_cert_validation_disabled(tmpdir, httpbin_secure, pool_mgr):
@@ -142,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,16 +1,14 @@
import http.client as httplib
import multiprocessing
from xmlrpc.client import ServerProxy
from xmlrpc.server import SimpleXMLRPCServer
import pytest
from six.moves import xmlrpc_client, xmlrpc_server
requests = pytest.importorskip("requests")
import vcr # NOQA
try:
import httplib
except ImportError:
import http.client as httplib
def test_domain_redirect():
"""Ensure that redirects across domains are considered unique"""
@@ -54,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):
@@ -66,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.
@@ -80,13 +81,13 @@ def test_amazon_doctype(tmpdir):
def start_rpc_server(q):
httpd = xmlrpc_server.SimpleXMLRPCServer(("127.0.0.1", 0))
httpd = SimpleXMLRPCServer(("127.0.0.1", 0))
httpd.register_function(pow)
q.put("http://{}:{}".format(*httpd.server_address))
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,))
@@ -99,11 +100,11 @@ def rpc_server():
def test_xmlrpclib(tmpdir, rpc_server):
with vcr.use_cassette(str(tmpdir.join("xmlrpcvideo.yaml"))):
roundup_server = xmlrpc_client.ServerProxy(rpc_server, allow_none=True)
roundup_server = ServerProxy(rpc_server, allow_none=True)
original_schema = roundup_server.pow(2, 4)
with vcr.use_cassette(str(tmpdir.join("xmlrpcvideo.yaml"))):
roundup_server = xmlrpc_client.ServerProxy(rpc_server, allow_none=True)
roundup_server = ServerProxy(rpc_server, allow_none=True)
second_schema = roundup_server.pow(2, 4)
assert original_schema == second_schema

View File

@@ -1,12 +1,13 @@
import contextlib
import copy
import http.client as httplib
import inspect
import os
from unittest import mock
from six.moves import http_client as httplib
import pytest
import yaml
from vcr.compat import mock, contextlib
from vcr.cassette import Cassette
from vcr.errors import UnhandledHTTPRequestError
from vcr.patch import force_reset
@@ -28,6 +29,19 @@ def test_cassette_load(tmpdir):
assert len(a_cassette) == 1
def test_cassette_load_nonexistent():
a_cassette = Cassette.load(path="something/nonexistent.yml")
assert len(a_cassette) == 0
def test_cassette_load_invalid_encoding(tmpdir):
a_file = tmpdir.join("invalid_encoding.yml")
with open(a_file, "wb") as fd:
fd.write(b"\xda")
a_cassette = Cassette.load(path=str(a_file))
assert len(a_cassette) == 0
def test_cassette_not_played():
a = Cassette("test")
assert not a.play_count
@@ -137,6 +151,31 @@ def test_cassette_all_played():
assert a.all_played
@mock.patch("vcr.cassette.requests_match", _mock_requests_match)
def test_cassette_allow_playback_repeats():
a = Cassette("test", allow_playback_repeats=True)
a.append("foo", "bar")
a.append("other", "resp")
for x in range(10):
assert a.play_response("foo") == "bar"
assert a.play_count == 10
assert a.all_played is False
assert a.play_response("other") == "resp"
assert a.play_count == 11
assert a.all_played
a.allow_playback_repeats = False
with pytest.raises(UnhandledHTTPRequestError) as e:
a.play_response("foo")
assert str(e.value) == "\"The cassette ('test') doesn't contain the request ('foo') asked for\""
a.rewind()
assert a.all_played is False
assert a.play_response("foo") == "bar"
assert a.all_played is False
assert a.play_response("other") == "resp"
assert a.all_played
@mock.patch("vcr.cassette.requests_match", _mock_requests_match)
def test_cassette_rewound():
a = Cassette("test")
@@ -183,7 +222,7 @@ def test_nesting_cassette_context_managers(*args):
)
assert_get_response_body_is("first_response")
# Make sure a second cassette can supercede the first
# Make sure a second cassette can supersede the first
with Cassette.use(path="test") as second_cassette:
with mock.patch.object(second_cassette, "play_response", return_value=second_response):
assert_get_response_body_is("second_response")
@@ -208,7 +247,7 @@ def test_nesting_context_managers_by_checking_references_of_http_connection():
def test_custom_patchers():
class Test(object):
class Test:
attribute = None
with Cassette.use(path="custom_patches", custom_patches=((Test, "attribute", VCRHTTPSConnection),)):
@@ -285,16 +324,16 @@ def test_func_path_generator():
def test_use_as_decorator_on_coroutine():
original_http_connetion = httplib.HTTPConnection
original_http_connection = httplib.HTTPConnection
@Cassette.use(inject=True)
def test_function(cassette):
assert httplib.HTTPConnection.cassette is cassette
assert httplib.HTTPConnection is not original_http_connetion
assert httplib.HTTPConnection is not original_http_connection
value = yield 1
assert value == 1
assert httplib.HTTPConnection.cassette is cassette
assert httplib.HTTPConnection is not original_http_connetion
assert httplib.HTTPConnection is not original_http_connection
value = yield 2
assert value == 2
@@ -308,15 +347,15 @@ def test_use_as_decorator_on_coroutine():
def test_use_as_decorator_on_generator():
original_http_connetion = httplib.HTTPConnection
original_http_connection = httplib.HTTPConnection
@Cassette.use(inject=True)
def test_function(cassette):
assert httplib.HTTPConnection.cassette is cassette
assert httplib.HTTPConnection is not original_http_connetion
assert httplib.HTTPConnection is not original_http_connection
yield 1
assert httplib.HTTPConnection.cassette is cassette
assert httplib.HTTPConnection is not original_http_connetion
assert httplib.HTTPConnection is not original_http_connection
yield 2
assert list(test_function()) == [1, 2]

View File

@@ -1,6 +1,7 @@
from unittest import mock
import pytest
from vcr.compat import mock
from vcr import errors
from vcr.cassette import Cassette

View File

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

@@ -1,10 +1,9 @@
import itertools
from vcr.compat import mock
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
@@ -16,9 +17,9 @@ def test_try_migrate_with_json(tmpdir):
cassette = tmpdir.join("cassette.json").strpath
shutil.copy("tests/fixtures/migration/old_cassette.json", cassette)
assert vcr.migration.try_migrate(cassette)
with open("tests/fixtures/migration/new_cassette.json", "r") as f:
with open("tests/fixtures/migration/new_cassette.json") as f:
expected_json = json.load(f)
with open(cassette, "r") as f:
with open(cassette) as f:
actual_json = json.load(f)
assert actual_json == expected_json
@@ -27,9 +28,9 @@ def test_try_migrate_with_yaml(tmpdir):
cassette = tmpdir.join("cassette.yaml").strpath
shutil.copy("tests/fixtures/migration/old_cassette.yaml", cassette)
assert vcr.migration.try_migrate(cassette)
with open("tests/fixtures/migration/new_cassette.yaml", "r") as f:
with open("tests/fixtures/migration/new_cassette.yaml") as f:
expected_yaml = yaml.load(f, Loader=Loader)
with open(cassette, "r") as f:
with open(cassette) as f:
actual_yaml = yaml.load(f, Loader=Loader)
assert actual_yaml == expected_yaml
@@ -44,4 +45,4 @@ def test_try_migrate_with_invalid_or_new_cassettes(tmpdir):
for file_path in files:
shutil.copy(file_path, cassette)
assert not vcr.migration.try_migrate(cassette)
assert filecmp.cmp(cassette, file_path) # shold not change file
assert filecmp.cmp(cassette, file_path) # should not change file

View File

@@ -1,6 +1,6 @@
import pytest
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

@@ -1,8 +1,4 @@
# coding: UTF-8
import io
import unittest
import six
from vcr.stubs import VCRHTTPResponse
@@ -58,7 +54,6 @@ def test_response_headers_should_have_correct_values():
assert response.headers.get("date") == "Fri, 24 Oct 2014 18:35:37 GMT"
@unittest.skipIf(six.PY2, "Regression test for Python3 only")
def test_response_parses_correctly_and_fp_attribute_error_is_not_thrown():
"""
Regression test for https://github.com/kevin1024/vcrpy/issues/440
@@ -97,7 +92,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

@@ -1,35 +1,35 @@
# -*- encoding: utf-8 -*-
from unittest import mock
import pytest
from vcr.compat import mock
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():
with open("tests/fixtures/migration/old_cassette.yaml", "r") as f:
with open("tests/fixtures/migration/old_cassette.yaml") as f:
with pytest.raises(ValueError):
deserialize(f.read(), yamlserializer)
def test_deserialize_old_json_cassette():
with open("tests/fixtures/migration/old_cassette.json", "r") as f:
with open("tests/fixtures/migration/old_cassette.json") as f:
with pytest.raises(ValueError):
deserialize(f.read(), jsonserializer)
def test_deserialize_new_yaml_cassette():
with open("tests/fixtures/migration/new_cassette.yaml", "r") as f:
with open("tests/fixtures/migration/new_cassette.yaml") as f:
deserialize(f.read(), yamlserializer)
def test_deserialize_new_json_cassette():
with open("tests/fixtures/migration/new_cassette.json", "r") as f:
with open("tests/fixtures/migration/new_cassette.json") as f:
deserialize(f.read(), jsonserializer)
REQBODY_TEMPLATE = u"""\
REQBODY_TEMPLATE = """\
interactions:
- request:
body: {req_body}

View File

@@ -1,17 +1,22 @@
from vcr.stubs import VCRHTTPSConnection
from vcr.compat import mock
from unittest import mock
from pytest import mark
from vcr import mode
from vcr.cassette import Cassette
from vcr.stubs import VCRHTTPSConnection
class TestVCRConnection(object):
def test_setting_of_attributes_get_propogated_to_real_connection(self):
class TestVCRConnection:
def test_setting_of_attributes_get_propagated_to_real_connection(self):
vcr_connection = VCRHTTPSConnection("www.examplehost.com")
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")
vcr_connection.cassette = Cassette("test", record_mode="all")
vcr_connection.cassette = Cassette("test", record_mode=mode.ALL)
vcr_connection.real_connection.connect()
assert vcr_connection.real_connection.sock is not None

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 @@
import http.client as httplib
import os
from pathlib import Path
from unittest import mock
import pytest
from six.moves import http_client as httplib
from vcr import VCR, use_cassette
from vcr.compat import mock
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():
@@ -31,7 +32,7 @@ def test_vcr_use_cassette():
function()
assert mock_cassette_load.call_args[1]["record_mode"] == test_vcr.record_mode
# Ensure that explicitly provided arguments still supercede
# Ensure that explicitly provided arguments still supersede
# those on the vcr.
new_record_mode = mock.Mock()
@@ -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:
@@ -170,7 +168,7 @@ def test_fixtures_with_use_cassette(random_fixture):
def test_custom_patchers():
class Test(object):
class Test:
attribute = None
attribute2 = None
@@ -188,11 +186,11 @@ def test_custom_patchers():
def test_inject_cassette():
vcr = VCR(inject_cassette=True)
@vcr.use_cassette("test", record_mode="once")
@vcr.use_cassette("test", record_mode=mode.ONCE)
def with_cassette_injected(cassette):
assert cassette.record_mode == "once"
assert cassette.record_mode == mode.ONCE
@vcr.use_cassette("test", record_mode="once", inject_cassette=False)
@vcr.use_cassette("test", record_mode=mode.ONCE, inject_cassette=False)
def without_cassette_injected():
pass
@@ -201,7 +199,7 @@ def test_inject_cassette():
def test_with_current_defaults():
vcr = VCR(inject_cassette=True, record_mode="once")
vcr = VCR(inject_cassette=True, record_mode=mode.ONCE)
@vcr.use_cassette("test", with_current_defaults=False)
def changing_defaults(cassette, checks):
@@ -212,10 +210,10 @@ def test_with_current_defaults():
checks(cassette)
def assert_record_mode_once(cassette):
assert cassette.record_mode == "once"
assert cassette.record_mode == mode.ONCE
def assert_record_mode_all(cassette):
assert cassette.record_mode == "all"
assert cassette.record_mode == mode.ALL
changing_defaults(assert_record_mode_once)
current_defaults(assert_record_mode_once)
@@ -226,7 +224,7 @@ def test_with_current_defaults():
def test_cassette_library_dir_with_decoration_and_no_explicit_path():
library_dir = "/libary_dir"
library_dir = "/library_dir"
vcr = VCR(inject_cassette=True, cassette_library_dir=library_dir)
@vcr.use_cassette()
@@ -237,7 +235,7 @@ def test_cassette_library_dir_with_decoration_and_no_explicit_path():
def test_cassette_library_dir_with_decoration_and_explicit_path():
library_dir = "/libary_dir"
library_dir = "/library_dir"
vcr = VCR(inject_cassette=True, cassette_library_dir=library_dir)
@vcr.use_cassette(path="custom_name")
@@ -248,7 +246,7 @@ def test_cassette_library_dir_with_decoration_and_explicit_path():
def test_cassette_library_dir_with_decoration_and_super_explicit_path():
library_dir = "/libary_dir"
library_dir = "/library_dir"
vcr = VCR(inject_cassette=True, cassette_library_dir=library_dir)
@vcr.use_cassette(path=os.path.join(library_dir, "custom_name"))
@@ -259,7 +257,7 @@ def test_cassette_library_dir_with_decoration_and_super_explicit_path():
def test_cassette_library_dir_with_path_transformer():
library_dir = "/libary_dir"
library_dir = "/library_dir"
vcr = VCR(
inject_cassette=True, cassette_library_dir=library_dir, path_transformer=lambda path: path + ".json"
)
@@ -360,3 +358,11 @@ def test_dynamically_added(self):
TestVCRClass.test_dynamically_added = test_dynamically_added
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"]

75
tox.ini
View File

@@ -1,13 +1,23 @@
[tox]
skip_missing_interpreters=true
envlist =
envlist =
cov-clean,
lint,
{py27,py35,py36,py37,py38,pypy,pypy3}-{requests,httplib2,urllib3,tornado4,boto3},
{py35,py36,py37,py38}-{aiohttp},
{py38,py39,py310,py311}-{requests-urllib3-1,httplib2,urllib3-1,tornado4,boto3,aiohttp,httpx},
{py310,py311}-{requests-urllib3-2,urllib3-2},
{pypy3}-{requests-urllib3-1,httplib2,urllib3-1,tornado4,boto3},
{py310}-httpx019,
cov-report
[gh-actions]
python =
3.8: py38
3.9: py39
3.10: py310, lint
3.11: py311
pypy-3: pypy3
# Coverage environment tasks: cov-clean and cov-report
# https://pytest-cov.readthedocs.io/en/latest/tox.html
[testenv:cov-clean]
@@ -18,7 +28,7 @@ commands = coverage erase
[testenv:cov-report]
deps = coverage
skip_install=true
commands =
commands =
coverage html
coverage report --fail-under=90
@@ -27,12 +37,37 @@ 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
deps =
flake8
black
isort
basepython = python3.10
[testenv:docs]
# Running sphinx from inside the "docs" directory
# ensures it will not pick up any stray files that might
# get into a virtual environment under the top-level directory
# or other artifacts under build/
changedir = docs
# The only dependency is sphinx
# If we were using extensions packaged separately,
# we would specify them here.
# A better practice is to specify a specific version of sphinx.
deps =
sphinx
sphinx_rtd_theme
# This is the sphinx command to generate HTML.
# In other circumstances, we might want to generate a PDF or an ebook
commands =
sphinx-build -W -b html -d {envtmpdir}/doctrees . {envtmpdir}/html
# We use Python 3.8. Tox sometimes tries to autodetect it based on the name of
# the testenv, but "docs" does not give useful clues so we have to be explicit.
basepython = python3.8
[testenv]
# Need to use develop install so that paths
@@ -40,28 +75,32 @@ deps =
usedevelop=true
commands =
./runtests.sh --cov=./vcr --cov-branch --cov-report=xml --cov-append {posargs}
allowlist_externals =
./runtests.sh
deps =
Flask
mock
Werkzeug==2.0.3
pytest
pytest-httpbin
pytest-httpbin>=1.0.1
pytest-cov
PyYAML
ipaddress
requests: requests>=2.22.0
httplib2: httplib2
urllib3: urllib3
{py27,py35,py36,pypy}-tornado4: tornado>=4,<5
{py27,py35,py36,pypy}-tornado4: pytest-tornado
{py27,py35,py36}-tornado4: pycurl
urllib3-1: urllib3<2
urllib3-2: urllib3<3
boto3: boto3
boto3: urllib3
aiohttp: aiohttp
aiohttp: pytest-asyncio
aiohttp: pytest-aiohttp
depends =
{py27,py35,py36,py37,pypy}-{lint,requests,httplib2,urllib3,tornado4,boto3},{py35,py36,py37}-{aiohttp}: cov-clean
cov-report: {py27,py35,py36,py37,pypy}-{lint,requests,httplib2,urllib3,tornado4,boto3},{py35,py36,py37}-{aiohttp}
httpx: httpx
{py38,py39,py310}-{httpx}: httpx
{py38,py39,py310}-{httpx}: pytest-asyncio
httpx: httpx>0.19
httpx019: httpx==0.19
{py38,py39,py310}-{httpx}: pytest-asyncio
depends =
lint,{py38,py39,py310,py311,pypy3}-{requests-urllib3-1,httplib2,urllib3-1,tornado4,boto3},{py310,py311}-{requests-urllib3-2,urllib3-2},{py38,py39,py310,py311}-{aiohttp},{py38,py39,py310,py311}-{httpx}: cov-clean
cov-report: lint,{py38,py39,py310,py311,pypy3}-{requests-urllib3-1,httplib2,urllib3-1,tornado4,boto3},{py310,py311}-{requests-urllib3-2,urllib3-2},{py38,py39,py310,py311}-{aiohttp}
passenv =
AWS_ACCESS_KEY_ID
AWS_DEFAULT_REGION

BIN
vcr.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 240 KiB

After

Width:  |  Height:  |  Size: 114 KiB

View File

@@ -1,23 +1,10 @@
import logging
import warnings
import sys
from logging import NullHandler
from .config import VCR
from .record_mode import RecordMode as mode # noqa import is not used in this file
# Set default logging handler to avoid "No handler found" warnings.
try: # Python 2.7+
from logging import NullHandler
except ImportError:
class NullHandler(logging.Handler):
def emit(self, record):
pass
if sys.version_info[0] == 2:
warnings.warn(
"Python 2.x support of vcrpy is deprecated and will be removed in an upcoming major release.",
DeprecationWarning,
)
__version__ = "5.0.0"
logging.getLogger(__name__).addHandler(NullHandler())

View File

@@ -1,17 +1,19 @@
import collections
import contextlib
import copy
import sys
import inspect
import logging
import sys
import wrapt
from .compat import contextlib
from ._handle_coroutine import handle_coroutine
from .errors import UnhandledHTTPRequestError
from .matchers import requests_match, uri, method, get_matchers_results
from .matchers import get_matchers_results, method, requests_match, uri
from .patch import CassettePatcherBuilder
from .persisters.filesystem import CassetteDecodeError, CassetteNotFoundError, FilesystemPersister
from .record_mode import RecordMode
from .serializers import yamlserializer
from .persisters.filesystem import FilesystemPersister
from .util import partition_dict
try:
@@ -22,18 +24,10 @@ except ImportError:
return False
if sys.version_info[:2] >= (3, 5):
from ._handle_coroutine import handle_coroutine
else:
def handle_coroutine(*args, **kwags):
raise NotImplementedError("Not implemented on Python 2")
log = logging.getLogger(__name__)
class CassetteContextDecorator(object):
class CassetteContextDecorator:
"""Context manager/decorator that handles installing the cassette and
removing cassettes.
@@ -51,7 +45,11 @@ class CassetteContextDecorator(object):
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):
@@ -61,6 +59,7 @@ class CassetteContextDecorator(object):
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:
@@ -70,9 +69,6 @@ class CassetteContextDecorator(object):
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
@@ -90,10 +86,22 @@ class CassetteContextDecorator(object):
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
@@ -159,7 +167,7 @@ class CassetteContextDecorator(object):
return new_args_getter
class Cassette(object):
class Cassette:
"""A container for recorded requests and responses"""
@classmethod
@@ -182,12 +190,13 @@ class Cassette(object):
path,
serializer=None,
persister=None,
record_mode="once",
record_mode=RecordMode.ONCE,
match_on=(uri, method),
before_record_request=None,
before_record_response=None,
custom_patches=(),
inject=False,
allow_playback_repeats=False,
):
self._persister = persister or FilesystemPersister
self._path = path
@@ -199,6 +208,7 @@ class Cassette(object):
self.inject = inject
self.record_mode = record_mode
self.custom_patches = custom_patches
self.allow_playback_repeats = allow_playback_repeats
# self.data is the list of (req, resp) tuples
self.data = []
@@ -213,7 +223,7 @@ class Cassette(object):
@property
def all_played(self):
"""Returns True if all responses have been played, False otherwise."""
return self.play_count == len(self)
return len(self.play_counts.values()) == len(self)
@property
def requests(self):
@@ -225,7 +235,7 @@ class Cassette(object):
@property
def write_protected(self):
return self.rewound and self.record_mode == "once" or self.record_mode == "none"
return self.rewound and self.record_mode == RecordMode.ONCE or self.record_mode == RecordMode.NONE
def append(self, request, response):
"""Add a request, response pair to this cassette"""
@@ -257,7 +267,7 @@ class Cassette(object):
def can_play_response_for(self, request):
request = self._before_record_request(request)
return request and request in self and self.record_mode != "all" and self.rewound
return request and request in self and self.record_mode != RecordMode.ALL and self.rewound
def play_response(self, request):
"""
@@ -265,12 +275,12 @@ class Cassette(object):
hasn't been played back before, and mark it as played
"""
for index, response in self._responses(request):
if self.play_counts[index] == 0:
if self.play_counts[index] == 0 or self.allow_playback_repeats:
self.play_counts[index] += 1
return response
# The cassette doesn't contain the request asked for.
raise UnhandledHTTPRequestError(
"The cassette (%r) doesn't contain the request (%r) asked for" % (self._path, request)
f"The cassette ({self._path!r}) doesn't contain the request ({request!r}) asked for"
)
def responses_of(self, request):
@@ -285,7 +295,7 @@ class Cassette(object):
return responses
# The cassette doesn't contain the request asked for.
raise UnhandledHTTPRequestError(
"The cassette (%r) doesn't contain the request (%r) asked for" % (self._path, request)
f"The cassette ({self._path!r}) doesn't contain the request ({request!r}) asked for"
)
def rewind(self):
@@ -342,11 +352,11 @@ class Cassette(object):
self.append(request, response)
self.dirty = False
self.rewound = True
except ValueError:
except (CassetteDecodeError, CassetteNotFoundError):
pass
def __str__(self):
return "<Cassette containing {} recorded response(s)>".format(len(self))
return f"<Cassette containing {len(self)} recorded response(s)>"
def __len__(self):
"""Return the number of request,response pairs stored in here"""
@@ -355,6 +365,6 @@ class Cassette(object):
def __contains__(self, request):
"""Return whether or not a request has been stored"""
for index, response in self._responses(request):
if self.play_counts[index] == 0:
if self.play_counts[index] == 0 or self.allow_playback_repeats:
return True
return False

View File

@@ -1,14 +0,0 @@
try:
from unittest import mock
except ImportError:
import mock
try:
import contextlib
except ImportError:
import contextlib2 as contextlib
else:
if not hasattr(contextlib, "ExitStack"):
import contextlib2 as contextlib
__all__ = ["mock", "contextlib"]

View File

@@ -1,25 +1,22 @@
import copy
try:
from collections import abc as collections_abc # only works on python 3.3+
except ImportError:
import collections 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 . import matchers
from . import filters
from .record_mode import RecordMode
from .serializers import jsonserializer, yamlserializer
from .util import auto_decorate, compose
class VCR(object):
class VCR:
@staticmethod
def is_test_method(method_name, function):
return method_name.startswith("test") and isinstance(function, types.FunctionType)
@@ -40,7 +37,7 @@ class VCR(object):
custom_patches=(),
filter_query_parameters=(),
ignore_hosts=(),
record_mode="once",
record_mode=RecordMode.ONCE,
ignore_localhost=False,
filter_headers=(),
before_record_response=None,
@@ -52,6 +49,7 @@ class VCR(object):
cassette_library_dir=None,
func_path_generator=None,
decode_compressed_response=False,
record_on_exception=True,
):
self.serializer = serializer
self.match_on = match_on
@@ -83,13 +81,14 @@ class VCR(object):
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):
try:
serializer = self.serializers[serializer_name]
except KeyError:
raise KeyError("Serializer {} doesn't exist or isn't registered".format(serializer_name))
raise KeyError(f"Serializer {serializer_name} doesn't exist or isn't registered")
return serializer
def _get_matchers(self, matcher_names):
@@ -98,11 +97,11 @@ class VCR(object):
for m in matcher_names:
matchers.append(self.matchers[m])
except KeyError:
raise KeyError("Matcher {} doesn't exist or isn't registered".format(m))
raise KeyError(f"Matcher {m} doesn't exist or isn't registered")
return matchers
def use_cassette(self, path=None, **kwargs):
if path is not None and not isinstance(path, six.string_types):
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)
@@ -126,6 +125,7 @@ class VCR(object):
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:
@@ -151,6 +151,8 @@ class VCR(object):
"inject": kwargs.get("inject_cassette", self.inject_cassette),
"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:
@@ -217,7 +219,7 @@ class VCR(object):
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
@@ -251,4 +253,5 @@ class VCR(object):
def test_case(self, predicate=None):
predicate = predicate or self.is_test_method
# TODO: Remove this reference to `six` in favor of the Python3 equivalent
return six.with_metaclass(auto_decorate(self.use_cassette, predicate))

View File

@@ -3,7 +3,7 @@ class CannotOverwriteExistingCassetteException(Exception):
self.cassette = kwargs["cassette"]
self.failed_request = kwargs["failed_request"]
message = self._get_message(kwargs["cassette"], kwargs["failed_request"])
super(CannotOverwriteExistingCassetteException, self).__init__(message)
super().__init__(message)
@staticmethod
def _get_message(cassette, failed_request):

View File

@@ -1,20 +1,18 @@
from six import BytesIO, text_type
from six.moves.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
def replace_headers(request, replacements):
"""
Replace headers in request according to replacements. The replacements
should be a list of (key, value) pairs where the value can be any of:
1. A simple replacement string value.
2. None to remove the given header.
3. A callable which accepts (key, value, request) and returns a string
value or None.
"""Replace headers in request according to replacements.
The replacements should be a list of (key, value) pairs where the value can be any of:
1. A simple replacement string value.
2. None to remove the given header.
3. A callable which accepts (key, value, request) and returns a string value or None.
"""
new_headers = request.headers.copy()
for k, rv in replacements:
@@ -37,10 +35,9 @@ def remove_headers(request, headers_to_remove):
def replace_query_parameters(request, replacements):
"""
Replace query parameters in request according to replacements. The
replacements should be a list of (key, value) pairs where the value can be
any of:
"""Replace query parameters in request according to replacements.
The replacements should be a list of (key, value) pairs where the value can be any of:
1. A simple replacement string value.
2. None to remove the given header.
3. A callable which accepts (key, value, request) and returns a string
@@ -73,10 +70,9 @@ def remove_query_parameters(request, query_parameters_to_remove):
def replace_post_data_parameters(request, replacements):
"""
Replace post data in request--either form data or json--according to
replacements. The replacements should be a list of (key, value) pairs where
the value can be any of:
"""Replace post data in request--either form data or json--according to replacements.
The replacements should be a list of (key, value) pairs where the value can be any of:
1. A simple replacement string value.
2. None to remove the given header.
3. A callable which accepts (key, value, request) and returns a string
@@ -88,7 +84,17 @@ def replace_post_data_parameters(request, replacements):
replacements = dict(replacements)
if request.method == "POST" and not isinstance(request.body, BytesIO):
if request.headers.get("Content-Type") == "application/json":
if isinstance(request.body, dict):
new_body = request.body.copy()
for k, rv in replacements.items():
if k in new_body:
ov = new_body.pop(k)
if callable(rv):
rv = rv(key=k, value=ov, request=request)
if rv is not None:
new_body[k] = rv
request.body = new_body
elif request.headers.get("Content-Type") == "application/json":
json_data = json.loads(request.body.decode("utf-8"))
for k, rv in replacements.items():
if k in json_data:
@@ -99,7 +105,7 @@ def replace_post_data_parameters(request, replacements):
json_data[k] = rv
request.body = json.dumps(json_data).encode("utf-8")
else:
if isinstance(request.body, text_type):
if isinstance(request.body, str):
request.body = request.body.encode("utf-8")
splits = [p.partition(b"=") for p in request.body.split(b"&")]
new_splits = []
@@ -144,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":
return zlib.decompress(body, zlib.MAX_WBITS | 16)
try:
return zlib.decompress(body, zlib.MAX_WBITS | 16)
except zlib.error:
return body # assumes that the data was already decompressed
else: # encoding == 'deflate'
return zlib.decompress(body)
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,42 +1,51 @@
import json
from six.moves import urllib, xmlrpc_client
from .util import read_body
import logging
import urllib
import xmlrpc.client
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(f"{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(f"{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(f"{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(f"{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(f"{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(f"{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(f"{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):
@@ -44,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(f"{r1.headers} != {r2.headers}")
def _header_checker(value, header="Content-Type"):
@@ -77,7 +88,7 @@ _checker_transformer_pairs = (
lambda body: urllib.parse.parse_qs(body.decode("ascii")),
),
(_header_checker("application/json"), _transform_json),
(lambda request: _xml_header_checker(request) and _xmlrpc_header_checker(request), xmlrpc_client.loads),
(lambda request: _xml_header_checker(request) and _xmlrpc_header_checker(request), xmlrpc.client.loads),
)
@@ -96,7 +107,7 @@ def _get_transformer(request):
def requests_match(r1, r2, matchers):
successes, failures = get_matchers_results(r1, r2, matchers)
if failures:
log.debug("Requests {} and {} differ.\n" "Failure details:\n" "{}".format(r1, r2, failures))
log.debug(f"Requests {r1} and {r2} differ.\nFailure details:\n{failures}")
return len(failures) == 0

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
@@ -54,7 +55,7 @@ def build_uri(**parts):
port = parts["port"]
scheme = parts["protocol"]
default_port = {"https": 443, "http": 80}[scheme]
parts["port"] = ":{}".format(port) if port != default_port else ""
parts["port"] = f":{port}" if port != default_port else ""
return "{protocol}://{host}{port}{path}".format(**parts)
@@ -117,7 +118,7 @@ def migrate(file_path, migration_fn):
# because we assume that original files can be reverted
# we will try to copy the content. (os.rename not needed)
with tempfile.TemporaryFile(mode="w+") as out_fp:
with open(file_path, "r") as in_fp:
with open(file_path) as in_fp:
if not migration_fn(in_fp, out_fp):
return False
with open(file_path, "w") as in_fp:
@@ -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]
@@ -149,7 +150,7 @@ def main():
for file_path in files:
migrated = try_migrate(file_path)
status = "OK" if migrated else "FAIL"
sys.stderr.write("[{}] {}\n".format(status, file_path))
sys.stderr.write(f"[{status}] {file_path}\n")
sys.stderr.write("Done.\n")

View File

@@ -1,12 +1,12 @@
"""Utilities for patching in cassettes"""
import contextlib
import functools
import http.client as httplib
import itertools
from .compat import contextlib, mock
from .stubs import VCRHTTPConnection, VCRHTTPSConnection
from six.moves import http_client as httplib
import logging
from unittest import mock
from .stubs import VCRHTTPConnection, VCRHTTPSConnection
log = logging.getLogger(__name__)
# Save some of the original types for the purposes of unpatching
@@ -15,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:
@@ -93,7 +98,16 @@ else:
_AiohttpClientSessionRequest = aiohttp.client.ClientSession._request
class CassettePatcherBuilder(object):
try:
import httpx
except ImportError: # pragma: no cover
pass
else:
_HttpxSyncClient_send = httpx.Client.send
_HttpxAsyncClient_send = httpx.AsyncClient.send
class CassettePatcherBuilder:
def _build_patchers_from_mock_triples_decorator(function):
@functools.wraps(function)
def wrapped(self, *args, **kwargs):
@@ -115,6 +129,7 @@ class CassettePatcherBuilder(object):
self._boto(),
self._tornado(),
self._aiohttp(),
self._httpx(),
self._build_patchers_from_mock_triples(self._cassette.custom_patches),
)
@@ -171,9 +186,7 @@ class CassettePatcherBuilder(object):
bases = (base_class,)
if not issubclass(base_class, object): # Check for old style class
bases += (object,)
return type(
"{}{}".format(base_class.__name__, self._cassette._path), bases, dict(cassette=self._cassette)
)
return type(f"{base_class.__name__}{self._cassette._path}", bases, dict(cassette=self._cassette))
@_build_patchers_from_mock_triples_decorator
def _httplib(self):
@@ -185,24 +198,15 @@ class CassettePatcherBuilder(object):
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)
pass
else:
from .stubs import boto3_stubs
@@ -244,12 +248,13 @@ class CassettePatcherBuilder(object):
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):
@@ -258,8 +263,7 @@ class CassettePatcherBuilder(object):
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
@@ -312,7 +316,22 @@ class CassettePatcherBuilder(object):
new_request = vcr_request(self._cassette, _AiohttpClientSessionRequest)
yield client.ClientSession, "_request", new_request
def _urllib3_patchers(self, cpool, stubs):
@_build_patchers_from_mock_triples_decorator
def _httpx(self):
try:
import httpx
except ImportError: # pragma: no cover
return
else:
from .stubs.httpx_stubs import async_vcr_send, sync_vcr_send
new_async_client_send = async_vcr_send(self._cassette, _HttpxAsyncClient_send)
yield httpx.AsyncClient, "send", new_async_client_send
new_sync_client_send = sync_vcr_send(self._cassette, _HttpxSyncClient_send)
yield httpx.Client, "send", new_sync_client_send
def _urllib3_patchers(self, cpool, conn, stubs):
http_connection_remover = ConnectionRemover(
self._get_cassette_subclass(stubs.VCRRequestsHTTPConnection)
)
@@ -320,9 +339,9 @@ class CassettePatcherBuilder(object):
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),
@@ -358,7 +377,7 @@ class CassettePatcherBuilder(object):
)
class ConnectionRemover(object):
class ConnectionRemover:
def __init__(self, connection_class):
self._connection_class = connection_class
self._connection_pool_to_connections = {}
@@ -392,69 +411,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)
pass
else:
if hasattr(cpool.AWSHTTPConnectionPool, "ConnectionCls"):
yield mock.patch.object(cpool.AWSHTTPConnectionPool, "ConnectionCls", _cpoolBoto3HTTPConnection)

View File

@@ -1,25 +1,40 @@
# .. _persister_example:
import os
from ..serialize import serialize, deserialize
from pathlib import Path
from ..serialize import deserialize, serialize
class FilesystemPersister(object):
class CassetteNotFoundError(FileNotFoundError):
pass
class CassetteDecodeError(ValueError):
pass
class FilesystemPersister:
@classmethod
def load_cassette(cls, cassette_path, serializer):
cassette_path = Path(cassette_path) # if cassette path is already Path this is no operation
if not cassette_path.is_file():
raise CassetteNotFoundError()
try:
with open(cassette_path) as f:
cassette_content = f.read()
except IOError:
raise ValueError("Cassette not found.")
cassette = deserialize(cassette_content, serializer)
return cassette
with cassette_path.open() as f:
data = f.read()
except UnicodeDecodeError as err:
raise CassetteDecodeError("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)

23
vcr/record_mode.py Normal file
View File

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

View File

@@ -1,13 +1,14 @@
import warnings
from six import BytesIO, text_type
from six.moves.urllib.parse import urlparse, parse_qsl
from .util import CaseInsensitiveDict
import logging
import warnings
from io import BytesIO
from urllib.parse import parse_qsl, urlparse
from .util import CaseInsensitiveDict
log = logging.getLogger(__name__)
class Request(object):
class Request:
"""
VCR's representation of a request.
"""
@@ -39,7 +40,7 @@ class Request(object):
@body.setter
def body(self, value):
if isinstance(value, text_type):
if isinstance(value, str):
value = value.encode("utf-8")
self._body = value
@@ -89,7 +90,7 @@ class Request(object):
return self.scheme
def __str__(self):
return "<Request ({}) {}>".format(self.method, self.uri)
return f"<Request ({self.method}) {self.uri}>"
def __repr__(self):
return self.__str__()
@@ -136,4 +137,4 @@ class HeadersDict(CaseInsensitiveDict):
if old:
key = old[0]
super(HeadersDict, self).__setitem__(key, value)
super().__setitem__(key, value)

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

@@ -1,6 +1,3 @@
import six
def convert_to_bytes(resp):
resp = convert_body_to_bytes(resp)
return resp
@@ -24,7 +21,7 @@ def convert_body_to_bytes(resp):
http://pyyaml.org/wiki/PyYAMLDocumentation#Python3support
"""
try:
if resp["body"]["string"] is not None and not isinstance(resp["body"]["string"], six.binary_type):
if resp["body"]["string"] is not None and not isinstance(resp["body"]["string"], bytes):
resp["body"]["string"] = resp["body"]["string"].encode("utf-8")
except (KeyError, TypeError, UnicodeEncodeError):
# The thing we were converting either wasn't a dictionary or didn't
@@ -44,7 +41,7 @@ def _convert_string_to_unicode(string):
result = string
try:
if string is not None and not isinstance(string, six.text_type):
if string is not None and not isinstance(string, str):
result = string.decode("utf-8")
except (TypeError, UnicodeDecodeError, AttributeError):
# Sometimes the string actually is binary or StringIO object,

View File

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

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,17 +1,18 @@
"""Stubs for patching HTTP and HTTPS requests"""
import logging
import six
from six.moves.http_client import HTTPConnection, HTTPSConnection, HTTPResponse
from six import BytesIO
from vcr.request import Request
from http.client import HTTPConnection, HTTPResponse, HTTPSConnection
from io import BytesIO
from vcr.errors import CannotOverwriteExistingCassetteException
from vcr.request import Request
from . import compat
log = logging.getLogger(__name__)
class VCRFakeSocket(object):
class VCRFakeSocket:
"""
A socket that doesn't do anything!
Used when playing back cassettes, when there
@@ -46,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
@@ -66,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
@@ -84,12 +87,15 @@ class VCRHTTPResponse(HTTPResponse):
def closed(self):
# in python3, I can't change the value of self.closed. So I'
# twiddling self._closed and using this property to shadow the real
# self.closed from the superclas
# self.closed from the superclass
return self._closed
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()
@@ -142,8 +148,37 @@ class VCRHTTPResponse(HTTPResponse):
def readable(self):
return self._content.readable()
@property
def length_remaining(self):
return self._content.getbuffer().nbytes - self._content.tell()
class VCRConnection(object):
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
cassette = None
@@ -153,26 +188,26 @@ class VCRConnection(object):
"""
port = self.real_connection.port
default_port = {"https": 443, "http": 80}[self._protocol]
return ":{}".format(port) if port != default_port else ""
return f":{port}" if port != default_port else ""
def _uri(self, url):
"""Returns request absolute URI"""
if url and not url.startswith("/"):
# Then this must be a proxy request.
return url
uri = "{}://{}{}{}".format(self._protocol, self.real_connection.host, self._port_postfix(), url)
uri = f"{self._protocol}://{self.real_connection.host}{self._port_postfix()}{url}"
log.debug("Absolute URI: %s", uri)
return uri
def _url(self, uri):
"""Returns request selector url from absolute URI"""
prefix = "{}://{}{}".format(self._protocol, self.real_connection.host, self._port_postfix())
prefix = f"{self._protocol}://{self.real_connection.host}{self._port_postfix()}"
return uri.replace(prefix, "", 1)
def request(self, method, url, body=None, headers=None, *args, **kwargs):
"""Persist the request metadata in self._vcr_request"""
self._vcr_request = Request(method=method, uri=self._uri(url), body=body, headers=headers or {})
log.debug("Got {}".format(self._vcr_request))
log.debug(f"Got {self._vcr_request}")
# Note: The request may not actually be finished at this point, so
# I'm not sending the actual request until getresponse(). This
@@ -188,7 +223,7 @@ class VCRConnection(object):
of putheader() calls.
"""
self._vcr_request = Request(method=method, uri=self._uri(url), body="", headers={})
log.debug("Got {}".format(self._vcr_request))
log.debug(f"Got {self._vcr_request}")
def putheader(self, header, *values):
self._vcr_request.headers[header] = values
@@ -220,7 +255,7 @@ class VCRConnection(object):
# Check to see if the cassette has a response for this request. If so,
# then return it
if self.cassette.can_play_response_for(self._vcr_request):
log.info("Playing response for {} from cassette".format(self._vcr_request))
log.info(f"Playing response for {self._vcr_request} from cassette")
response = self.cassette.play_response(self._vcr_request)
return VCRHTTPResponse(response)
else:
@@ -232,7 +267,7 @@ class VCRConnection(object):
# Otherwise, we should send the request, then get the response
# and return it.
log.info("{} not in cassette, sending to real server".format(self._vcr_request))
log.info(f"{self._vcr_request} not in cassette, sending to real server")
# This is imported here to avoid circular import.
# TODO(@IvanMalison): Refactor to allow normal import.
from vcr.patch import force_reset
@@ -247,12 +282,13 @@ class VCRConnection(object):
# 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)
@@ -296,8 +332,7 @@ class VCRConnection(object):
self.real_connection.sock = value
def __init__(self, *args, **kwargs):
if six.PY3:
kwargs.pop("strict", None) # apparently this is gone in py3
kwargs.pop("strict", None) # apparently this is gone in py3
# need to temporarily reset here because the real connection
# inherits from the thing that we are mocking out. Take out
@@ -312,7 +347,7 @@ class VCRConnection(object):
def __setattr__(self, name, value):
"""
We need to define this because any attributes that are set on the
VCRConnection need to be propogated to the real connection.
VCRConnection need to be propagated to the real connection.
For example, urllib3 will set certain attributes on the connection,
such as 'ssl_version'. These attributes need to get set on the real
@@ -328,7 +363,7 @@ class VCRConnection(object):
# we're setting the real_connection itself for the first time
pass
super(VCRConnection, self).__setattr__(name, value)
super().__setattr__(name, value)
def __getattr__(self, name):
"""
@@ -340,7 +375,7 @@ class VCRConnection(object):
# we're setting the real_connection itself for the first time
return getattr(self.real_connection, name)
return super(VCRConnection, self).__getattr__(name)
return super().__getattr__(name)
for k, v in HTTPConnection.__dict__.items():

View File

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

View File

@@ -1,19 +1,6 @@
"""Stubs for boto3"""
import six
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
@@ -26,8 +13,7 @@ class VCRRequestsHTTPSConnection(VCRHTTPSConnection, VerifiedHTTPSConnection):
_baseclass = VerifiedHTTPSConnection
def __init__(self, *args, **kwargs):
if six.PY3:
kwargs.pop("strict", None) # apparently this is gone in py3
kwargs.pop("strict", None)
# need to temporarily reset here because the real connection
# inherits from the thing that we are mocking out. Take out

View File

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

View File

@@ -1,12 +1,5 @@
import six
from six import BytesIO
from six.moves.http_client import HTTPMessage
try:
import http.client
except ImportError:
pass
import http.client
from io import BytesIO
"""
The python3 http.client api moved some stuff around, so this is an abstraction
@@ -15,30 +8,19 @@ layer that tries to cope with this move.
def get_header(message, name):
if six.PY3:
return message.getallmatchingheaders(name)
else:
return message.getheader(name)
return message.getallmatchingheaders(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
def get_headers(message):
for key in set(message.keys()):
if six.PY3:
yield key, message.get_all(key)
else:
yield key, message.getheaders(key)
yield key, message.get_all(key)
def get_httpmessage(headers):
if six.PY3:
return http.client.parse_headers(BytesIO(headers))
msg = HTTPMessage(BytesIO(headers))
msg.fp.seek(0)
msg.readheaders()
return msg
return http.client.parse_headers(BytesIO(headers))

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",

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

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

View File

@@ -1,9 +1,6 @@
"""Stubs for requests"""
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,8 +1,6 @@
"""Stubs for tornado HTTP clients"""
from __future__ import absolute_import
import functools
from six import BytesIO
from io import BytesIO
from tornado import httputil
from tornado.httpclient import HTTPResponse

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 f"{self.__class__.__name__}.{self._testMethodName}.yaml"
class VCRTestCase(VCRMixin, unittest.TestCase):
pass

View File

@@ -1,9 +1,5 @@
import types
try:
from collections.abc import Mapping, MutableMapping
except ImportError:
from collections import Mapping, MutableMapping
from collections.abc import Mapping, MutableMapping
# Shamelessly stolen from https://github.com/kennethreitz/requests/blob/master/requests/structures.py
@@ -107,12 +103,12 @@ def auto_decorate(decorator, predicate=lambda name, value: isinstance(value, typ
class DecorateAll(type):
def __setattr__(cls, attribute, value):
return super(DecorateAll, cls).__setattr__(attribute, maybe_decorate(attribute, value))
return super().__setattr__(attribute, maybe_decorate(attribute, value))
def __new__(cls, name, bases, attributes_dict):
new_attributes_dict = {
attribute: maybe_decorate(attribute, value) for attribute, value in attributes_dict.items()
}
return super(DecorateAll, cls).__new__(cls, name, bases, new_attributes_dict)
return super().__new__(cls, name, bases, new_attributes_dict)
return DecorateAll