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

Compare commits

...

1026 Commits

Author SHA1 Message Date
Kevin McCarthy
c7bb59ecd8 Release v8.0.0 2025-12-03 13:15:37 -05:00
pre-commit
2183690da3 pre-commit: Autoupdate 2025-11-24 08:09:51 -03:00
Matthias (~talfus-laddus)
d5ba702a1b only log message if response is appended
Closes: https://github.com/kevin1024/vcrpy/issues/685
2025-11-19 13:53:06 -03:00
Seow Alex
952994b365 Patch httpcore instead of httpx 2025-11-19 13:44:40 -03:00
Jair Henrique
e2f3240835 Fixes some tornado tests 2025-11-19 12:18:05 -03:00
Jair Henrique
bb690833bc Enables SIM ruff lint 2025-11-19 12:18:05 -03:00
Jair Henrique
73eed94c47 Drops Python 3.9 support 2025-11-19 12:18:05 -03:00
dependabot[bot]
a23fe0333a build(deps): bump actions/setup-python from 5 to 6
Bumps [actions/setup-python](https://github.com/actions/setup-python) from 5 to 6.
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](https://github.com/actions/setup-python/compare/v5...v6)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-19 11:14:40 -03:00
dependabot[bot]
bb743861b6 build(deps): bump astral-sh/setup-uv from 6 to 7
Bumps [astral-sh/setup-uv](https://github.com/astral-sh/setup-uv) from 6 to 7.
- [Release notes](https://github.com/astral-sh/setup-uv/releases)
- [Commits](https://github.com/astral-sh/setup-uv/compare/v6...v7)

---
updated-dependencies:
- dependency-name: astral-sh/setup-uv
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-19 06:57:48 -03:00
dependabot[bot]
ac70eaa17f build(deps): bump astral-sh/setup-uv from 5 to 6
Bumps [astral-sh/setup-uv](https://github.com/astral-sh/setup-uv) from 5 to 6.
- [Release notes](https://github.com/astral-sh/setup-uv/releases)
- [Commits](https://github.com/astral-sh/setup-uv/compare/v5...v6)

---
updated-dependencies:
- dependency-name: astral-sh/setup-uv
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-11 16:57:56 -03:00
dependabot[bot]
d50f3385a6 build(deps): bump actions/checkout from 4 to 5
Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 5.
- [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/v4...v5)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-26 09:37:05 -03:00
Jair Henrique
14db4de224 Use uv on CI 2025-08-26 09:35:26 -03:00
Sebastian Pipping
2c4df79498 Merge pull request #917 from kevin1024/precommit-autoupdate
pre-commit: Autoupdate
2025-08-01 19:17:52 +02:00
pre-commit
1456673cb4 pre-commit: Autoupdate 2025-08-01 16:06:54 +00:00
pre-commit
19bd4e012c pre-commit: Autoupdate 2025-03-23 16:52:07 -03:00
Karolina Surma
558c7fc625 Import iscoroutinefunction() from inspect rather than asyncio
The asyncio function is deprecated starting from Python 3.14 and
will be removed from Python 3.16.
2025-03-23 16:49:33 -03:00
Sebastian Pipping
8217a4c21b Merge pull request #903 from kevin1024/precommit-autoupdate
pre-commit: Autoupdate
2025-01-17 21:03:56 +01:00
pre-commit
bd0aa59cd2 pre-commit: Autoupdate 2025-01-17 20:50:18 +01:00
Sebastian Pipping
9a37817a3a Merge pull request #904 from kevin1024/fix-ci
Fix CI for error `unshare: write failed /proc/self/uid_map: Operation not permitted` with Ubuntu >=24.04
2025-01-17 20:49:41 +01:00
Sebastian Pipping
b4c65bd677 main.yml: Allow creation of user namespaces to unshare in Ubuntu >=24.04 2025-01-17 20:36:18 +01:00
Sebastian Pipping
93bc59508c Pin GitHub Actions and Read the Docs to explicit Ubuntu 24.04 2025-01-17 20:16:30 +01:00
pre-commit
e313a9cd52 pre-commit: Autoupdate 2025-01-12 09:28:43 -03:00
Sebastian Pipping
5f1b20c4ca Merge pull request #763 from danielnsilva/drop-unused-requests
Add an option to remove unused requests from cassette
2025-01-11 20:51:28 +01:00
Daniel Silva
cd31d71901 refactor: move logic for building used interactions dict before saving 2025-01-11 16:56:59 +00:00
Daniel Silva
4607ca1102 fix: add drop_unused_requests check in cassette saving logic 2025-01-11 16:55:04 +00:00
Daniel Silva
e3ced4385e docs: update example in advanced.rst
Co-authored-by: Sebastian Pipping <sebastian@pipping.org>
2025-01-11 11:21:55 -05:00
Jair Henrique
80099ac6d7 Clean pytest configurations 2025-01-08 16:16:56 -03:00
Sebastian Pipping
440bc20faf Merge pull request #809 from alga/alga-https-proxy
Fix HTTPS proxy handling
2025-01-08 00:20:54 +01:00
Albertas Agejevas
3ddff27cda Remove redundant assertions.
They are covered by the next line.
2025-01-07 21:48:24 +02:00
Albertas Agejevas
30b423e8c0 Use mode="none" in proxy tests as suggested by @hartwork. 2025-01-07 21:48:24 +02:00
Albertas Agejevas
752ba0b749 Fix HTTPS proxy handling. 2025-01-07 21:48:24 +02:00
Albertas Agejevas
c16e526d6a Integration test for HTTPS proxy handling. 2025-01-07 21:48:24 +02:00
Daniel Silva
d64cdd337b style: fix formatting issues to comply with pre-commit hooks 2025-01-04 23:45:43 +00:00
Martin Brunthaler
ac230b76af Call urllib.parse less frequently 2025-01-04 15:42:49 -03:00
pre-commit
965f3658d5 pre-commit: Autoupdate 2025-01-04 15:21:25 -03:00
Jair Henrique
6465a5995b Fix docs conf 2025-01-04 15:19:50 -03:00
dependabot[bot]
69ca261a88 build(deps): bump sphinx-rtd-theme from 2.0.0 to 3.0.2
Bumps [sphinx-rtd-theme](https://github.com/readthedocs/sphinx_rtd_theme) from 2.0.0 to 3.0.2.
- [Changelog](https://github.com/readthedocs/sphinx_rtd_theme/blob/master/docs/changelog.rst)
- [Commits](https://github.com/readthedocs/sphinx_rtd_theme/compare/2.0.0...3.0.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-01-04 15:19:50 -03:00
kevin1024
3278619dcc Release v7.0.0 2024-12-30 18:59:50 -05:00
Aleksei Kozharin
3fb62e0f9b fix: correctly handle asyncio.run when loop exists 2024-12-30 13:34:03 -03:00
dependabot[bot]
81978659f1 build(deps): update sphinx requirement from <8 to <9
Updates the requirements on [sphinx](https://github.com/sphinx-doc/sphinx) to permit the latest version.
- [Release notes](https://github.com/sphinx-doc/sphinx/releases)
- [Changelog](https://github.com/sphinx-doc/sphinx/blob/v8.0.2/CHANGES.rst)
- [Commits](https://github.com/sphinx-doc/sphinx/compare/v0.1.61611...v8.0.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-28 14:03:41 -03:00
pre-commit
be651bd27c pre-commit: Autoupdate 2024-12-28 13:48:49 -03:00
Jair Henrique
a6698ed060 Fix aiohttp tests 2024-12-28 13:42:54 -03:00
Igor Gumenyuk
48d0a2e453 Fixed missing version_string attribute when used with urllib3>=2.3.0
urllib3 in v2.3.0 introduced attribute `version_string` (https://github.com/urllib3/urllib3/pull/3316/files). This attribute  is missing in `VCRHTTPResponse` which causes errors like AttributeError: 'VCRHTTPResponse' object has no attribute 'version_string'

This fixes https://github.com/kevin1024/vcrpy/issues/888
2024-12-28 13:39:53 -03:00
Jair Henrique
5b858b132d Fix lint 2024-12-28 13:25:04 -03:00
Jair Henrique
c8d99a99ec Fix ruff configuration 2024-12-28 13:21:15 -03:00
Sebastian Pipping
ce27c63685 Merge pull request #736 from kevin1024/drop-python38
[14 Oct 2024] Drop python 3.8 support
2024-10-13 04:05:13 +02:00
Jair Henrique
ab8944d3ca Drop python 3.8 support 2024-10-12 22:44:42 -03:00
Sebastian Pipping
c6a7f4ae15 Merge pull request #872 from kevin1024/precommit-autoupdate
pre-commit: Autoupdate
2024-10-11 01:22:08 +02:00
Kevin McCarthy
1d100dda25 release v6.0.2 2024-10-07 08:55:44 -04:00
pre-commit
7275e5d65d pre-commit: Autoupdate 2024-10-04 16:05:32 +00:00
Sebastian Pipping
c6be705fb4 Merge pull request #871 from kevin1024/precommit-autoupdate
pre-commit: Autoupdate
2024-09-20 22:37:01 +02:00
pre-commit
10b7f4efb3 pre-commit: Autoupdate 2024-09-20 16:05:30 +00:00
Sebastian Pipping
7a6ef00f4d Merge pull request #870 from kevin1024/dependabot/github_actions/peter-evans/create-pull-request-7
build(deps): bump peter-evans/create-pull-request from 6 to 7
2024-09-19 00:33:49 +02:00
Sebastian Pipping
3bf6ac7184 Merge pull request #867 from kevin1024/precommit-autoupdate
pre-commit: Autoupdate
2024-09-19 00:32:45 +02:00
pre-commit
983b2202ed pre-commit: Autoupdate 2024-09-19 00:28:52 +02:00
dependabot[bot]
15a6b71997 build(deps): bump peter-evans/create-pull-request from 6 to 7
Bumps [peter-evans/create-pull-request](https://github.com/peter-evans/create-pull-request) from 6 to 7.
- [Release notes](https://github.com/peter-evans/create-pull-request/releases)
- [Commits](https://github.com/peter-evans/create-pull-request/compare/v6...v7)

---
updated-dependencies:
- dependency-name: peter-evans/create-pull-request
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-09-18 22:27:51 +00:00
Sebastian Pipping
1ca708dcff Merge pull request #862 from kevin1024/test-on-313
Start testing with CPython 3.13
2024-09-19 00:00:50 +02:00
Thomas Grainger
f5597fa6c1 use pytest_httbin.certs.where() for cafile 2024-09-18 22:36:17 +01:00
Thomas Grainger
2b3247b3df remove redundant load_cert_chain 2024-09-18 22:35:15 +01:00
Thomas Grainger
d123a5e8d0 replace fixture with constant 2024-09-18 22:34:48 +01:00
Thomas Grainger
e2815fbc88 move httbin_ssl_context fixture into the one place it's used 2024-09-18 22:31:36 +01:00
Thomas Grainger
f9d4500c6e test on 3.13 2024-09-18 19:24:37 +01:00
Sebastian Pipping
71eb624708 Merge pull request #851 from sathieu/consume-body-once2
Ensure body is consumed only once (alternative to #847)
2024-08-02 19:16:38 +02:00
Sebastian Pipping
dc449715c1 Merge pull request #864 from kevin1024/precommit-autoupdate
pre-commit: Autoupdate
2024-08-02 19:15:52 +02:00
pre-commit
275b9085f3 pre-commit: Autoupdate 2024-08-02 16:05:06 +00:00
Thomas Grainger
35650b141b Merge pull request #856 from connortann/patch-1
Fix package install: remove use of deprecated setuptools.command.test
2024-07-29 17:35:23 +01:00
Thomas Grainger
9c8b679136 remove unused sys import from setup.py 2024-07-29 17:27:19 +01:00
connortann
fab082eff5 Update setup.py: remove use of deprecated setuptools.command.test 2024-07-29 11:10:18 +01:00
Sebastian Pipping
ffc04f9128 Merge pull request #853 from kevin1024/precommit-autoupdate
pre-commit: Autoupdate
2024-07-27 01:25:13 +02:00
pre-commit
4d84da1809 pre-commit: Autoupdate 2024-07-26 16:05:08 +00:00
Mathieu Parent
241b0bbd91 Ensure body is consumed only once
Fixes: #846
Signed-off-by: Mathieu Parent <math.parent@gmail.com>
2024-07-21 22:53:45 +02:00
Sebastian Pipping
042e16c3e4 Merge pull request #850 from kevin1024/precommit-autoupdate
pre-commit: Autoupdate
2024-07-13 21:52:05 +02:00
pre-commit
acef3f49bf pre-commit: Autoupdate 2024-07-05 16:05:04 +00:00
Sebastian Pipping
9cfa6c5173 Merge pull request #845 from kevin1024/precommit-autoupdate
pre-commit: Autoupdate
2024-06-30 15:24:31 +02:00
Sebastian Pipping
39a86ba3cf pre-commit: Fix Ruff invocation for >=0.5.0
Related changes in Ruff:
https://github.com/astral-sh/ruff/pull/9687
2024-06-30 15:18:35 +02:00
pre-commit
543c72ba51 pre-commit: Autoupdate 2024-06-28 16:04:49 +00:00
Sebastian Pipping
86b114f2f5 Merge pull request #831 from kevin1024/precommit-autoupdate
pre-commit: Autoupdate
2024-06-02 15:33:33 +02:00
Sebastian Pipping
4b06f3dba1 Merge pull request #836 from chuckwondo/fix-testing-instructions
Fix typos in testing instructions
2024-06-02 15:29:57 +02:00
pre-commit
1c6503526b pre-commit: Autoupdate 2024-06-02 15:27:57 +02:00
Sebastian Pipping
c9c05682cb Merge pull request #843 from kevin1024/fix-ci-through-recent-setuptools
Fix CI through recent Setuptools + start building once a week to notice CI issues earlier
2024-06-02 15:27:23 +02:00
Sebastian Pipping
39c8648aa7 main.yml: Make sure that build issues do not go unnoticed for >7 days 2024-06-02 15:23:51 +02:00
Sebastian Pipping
dfff84d5bb main.yml: Fix "pypy-3.9,urllib3<2" CI through recent Setuptools
Command "pip install codecov '.[tests]' 'urllib3<2'" was failing
with error:
> hpy 0.9.0 has requirement setuptools>=64.0, but you have
> setuptools 58.1.0.
2024-06-02 15:23:08 +02:00
Chuck Daniels
40ac0de652 Fix typos in test commands 2024-04-21 19:58:00 -04:00
Sebastian Pipping
f3147f574b Merge pull request #830 from pjonsson/permit-urllib3
Permit urllib3 >=2 for non-PyPy Python >=3.10 in order to help users of Poetry
2024-03-11 19:35:43 +01:00
Sebastian Pipping
298a6933ff Merge pull request #829 from kevin1024/precommit-autoupdate
pre-commit: Autoupdate
2024-03-10 23:17:01 +01:00
Peter A. Jonsson
52da776b59 Permit urllib3 2.x for non-PyPy Python >=3.10
Poetry makes platform indepdent lock files,
so the PyPy marker is there even when using
CPython >= 3.10.

Add a third constraint that permits any urllib3
version when using Python >=3.10 and some
other implementation than PyPy.
2024-03-10 22:37:04 +01:00
pre-commit
8842fb1c3a pre-commit: Autoupdate 2024-03-08 16:04:43 +00:00
Sebastian Pipping
6c4ba172d8 Merge pull request #827 from kevin1024/precommit-autoupdate
pre-commit: Autoupdate
2024-03-06 14:40:32 +01:00
pre-commit
c88f2c0dab pre-commit: Mass-apply ruff formatter 2024-03-06 14:35:01 +01:00
pre-commit
3fd6b1c0b4 pre-commit: Autoupdate 2024-03-01 16:04:42 +00:00
Sebastian Pipping
c6d87309f4 Merge pull request #823 from mgorny/httpbin-compat
Improve test compatibility with legacy httpbin index
2024-02-28 20:38:23 +01:00
Sebastian Pipping
1fb9179cf9 Merge pull request #813 from kevin1024/precommit-autoupdate
pre-commit: Autoupdate
2024-02-28 20:37:08 +01:00
pre-commit
a58e0d8830 pre-commit: Autoupdate 2024-02-23 16:04:44 +00:00
dependabot[bot]
acc101412d build(deps): bump pre-commit/action from 3.0.0 to 3.0.1
Bumps [pre-commit/action](https://github.com/pre-commit/action) from 3.0.0 to 3.0.1.
- [Release notes](https://github.com/pre-commit/action/releases)
- [Commits](https://github.com/pre-commit/action/compare/v3.0.0...v3.0.1)

---
updated-dependencies:
- dependency-name: pre-commit/action
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-02-21 10:39:22 -03:00
Michał Górny
e60dafb8dc Improve test compatibility with legacy httpbin index
Make the tests slightly more flexible to match both the flasgger-based
and legacy httpbin index.  This is needed for compatibility with
https://github.com/psf/httpbin/pull/44 when flasgger is not installed
(e.g. on architectures that are not supported by Rust).
2024-02-16 19:33:41 +01:00
dependabot[bot]
3ce5979acb build(deps): bump peter-evans/create-pull-request from 5 to 6
Bumps [peter-evans/create-pull-request](https://github.com/peter-evans/create-pull-request) from 5 to 6.
- [Release notes](https://github.com/peter-evans/create-pull-request/releases)
- [Commits](https://github.com/peter-evans/create-pull-request/compare/v5...v6)

---
updated-dependencies:
- dependency-name: peter-evans/create-pull-request
  dependency-type: direct:production
  update-type: version-update:semver-major
...

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

```py
class ConnectionRemover:
    ...

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

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

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

Add `_old_interactions`, `_played_interactions` and `_new_interactions()`.  The `_old_interactions` are previously recorded interactions loaded from Cassette files. The `_played_interactions` is a set of old interactions that were marked as played.  A new interaction is a tuple (request, response) in `self.data` that is not in `_old_interactions` list.
2023-01-02 03:52:52 +00: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
Josh Peak
8245bd4f84 v3.0.0 RC (#498)
* v3.0.0 RC

* Add credit for documentation improvements for @yarikoptic
2019-12-14 19:49:50 +11:00
Yaroslav Halchenko
531685d50b DOC(FIX): fixed variable name (a -> cass) in an example for rewind (#492) 2019-12-13 15:16:42 +11:00
Nick DiRienzo
9ec19dd966 aiohttp: fix multiple requests being replayed per request and add support for request_info on mocked responses (#495)
* Fix how redirects are handled so we can have requests with the same URL be used distinctly

* Add support for request_info. Remove `past` kwarg.

* Remove as e to make linter happy

* Add unreleased 3.0.0 to changelog.
2019-12-13 15:01:17 +11:00
Josh Peak
ffd2142d86 Patch release 2.1.1 (#490) 2019-11-03 21:35:39 +11:00
Simone Orsi
8e78666b8a Fix header matcher for boto3 (fixes #474) (#488) 2019-11-03 21:00:33 +11:00
Josh Peak
d843d2a6c1 Fix yarl version requirements (#489) 2019-11-03 18:15:08 +11:00
Arthur Hamon
0d623aead5 Merge pull request #483 from Stranger6667/alternative-pytest-plugin
Add `pytest-recording` to the documentation as an alternative Pytest plugin
2019-10-01 14:48:03 +02:00
dmitry.dygalo
f263c60d78 Add pytest-recording to the documentation as an alternative Pytest plugin 2019-10-01 11:22:28 +02:00
Arthur Hamon
2eba71092d Merge pull request #484 from sdvicorp/keith/changelog_empty_post
Update changelog for empty body fix
2019-10-01 09:00:39 +02:00
Keith Prickett
dd277d7a30 Update changelog for empty body fix 2019-10-01 06:24:17 +00:00
Arthur Hamon
dc9fbef3f0 Merge pull request #423 from sdvicorp/keith/fix_empty_post
Fix exception when body of request is empty.
2019-09-22 10:08:23 +02:00
Keith Prickett
f31be550bd Rebase on master
- Cleaned up fix to prevent additional nesting
- Added unit test

Fixes error:

```
>               splits = [p.partition(b"=") for p in request.body.split(b"&")]
E               AttributeError: 'NoneType' object has no attribute 'split'
```
2019-09-17 23:28:49 +00:00
Josh Peak
baadf913ef Update changelog.rst (#480) 2019-08-29 05:00:16 +10:00
Greg Ward
1eec0f657d Clarify documentation of custom request matchers (#479)
* The choice is not what the function *returns*, but what it *does*
  (raising an exception is not returning a value).

* Recommend "assert" more strongly, and additionally recommend an
  error message with the assert.

* Back up that recommendation in the sample code.
2019-08-29 04:53:48 +10:00
Josh Peak
c5c120e91b Add travis testing support for python 3.8 (#477)
* Add travis testing support for python 3.8

* Use Xenial for all

* Simplify matrix

* Consistent indents

* Add py38 to tox and update contributing guide for how to use pyenv

* Fix travis spec to allow 3.8-dev to fail and tox should pass 90% coverage for full suite
2019-08-25 20:50:48 +10:00
Josh Peak
7caf29735a Format project with black (#467)
Format with line length 110 to match flake8

make black part of linting check

Update travis spec for updated black requirements

Add diff output for black on failure

update changelog
2019-08-24 11:36:35 +10:00
Hugo van Kemenade
75969de601 Use generic pypy3 for latest version (#473) 2019-08-24 09:59:36 +10:00
Josh Peak
78e21aa220 Increment version to v2.1.0 and add Python2.x deprecation warning (#458)
* Increment version and add Python2.x deprecation warning

Change test to actually test no warnings when on python3

flake8 compliant

* fix development status classifier
2019-08-08 18:00:41 +10:00
Arthur Hamon
1b565d3f88 Feat/2.1.0 release changelog documentation (#466)
* chore(changelog): add changelog for 2.1.0 release

* doc(rewind): add documentation a new feature `rewind` that can replay a cassette

* doc(exception): add documentation on the new behavior of the CannotOverwriteExistingCassetteException message
2019-08-08 16:37:55 +10:00
Josh Peak
cc752cf790 Collect and aggregate code coverage across all test environments (#460)
* Collect and aggregate code coverage across all test environments

Add Coveralls badge to readme

Try CodeCov integration

Setup coverage badge for CodeCov

Fix CodeCov Badge

* Set code coverage regression threshold

Attempt to cache bust some boto3 issues on TravisCI

* Disable legacy secure strings

* Skip boto3 tests on TRAVIS_PULL_REQUEST due to credentials being unavailable

Address flake8 issues

Debug TRAVIS_PULL_REQUEST ENV Var

Trying to get boto3 tests to skip on forked PRs

boto3 is not an allowed failure

remove TRAVIS ENV debugging

* remove pytest skipif that wasn't working

* Ignore coverage files
2019-08-06 16:41:42 +10:00
Arthur Hamon
caa3a67bde feat(failure-message): reformat the message with the best requests matches (#459)
The message is less verbose and the display is improved for a better readability.
2019-08-06 09:57:13 +10:00
Laurent Mazuel
e3b7116564 aiohttp headers are case insensitive (#461)
* aiohttp headers are case insensitive

* flakes
2019-07-31 09:17:54 +10:00
Arthur Hamon
9d37210fc8 Merge pull request #451 from neozenith/master
fix IndexError when empty cassette throws CannotOverwriteCassetteException
2019-07-25 08:11:01 +02:00
Josh Peak
857488ee3a Add pytest-cov to check coverage of tox installed library 2019-07-25 11:26:49 +10:00
Josh Peak
9fb8a7b0ba Add guard statement for empty Cassette when returning best match
fix white space flake 8 issue
2019-07-24 20:06:44 +10:00
Josh Peak
77581437f7 fix IndexError when empty cassette throws CannotOverwriteCassetteException 2019-07-24 20:06:44 +10:00
Arthur Hamon
759423a75a Merge pull request #457 from stj/fix-log-formatting
Fix log format bug
2019-07-17 18:05:38 +02:00
Stefan Tjarks
988ac335d4 Fix log format bug
`{}` is not a valid placeholder in log formatting.
2019-07-17 07:46:40 -07:00
Arthur Hamon
200262fb5c Merge pull request #411 from jans-forks/fix/382-boto3
Fixes #382 boto3 compatibility.
2019-07-15 19:38:56 +02:00
Arthur Hamon
8c54ae9826 Merge pull request #456 from kevin1024/fix-aiohttp-stub-to-record-redirects
Fix aiohttp stub to record intermediary redirect responses properly
2019-07-13 19:18:12 +02:00
Arthur Hamon
2d96d35621 delete test_boto3_without_vcr test as it does not test vcr module 2019-07-13 14:13:43 +02:00
Arthur Hamon
6ca7cf3cc6 refactor boto3 integration test using fixtures instead of global variables 2019-07-13 14:13:43 +02:00
Arthur Hamon
5e76e4733d fix real connection on boto3 stub, adding not set parameters manually
Set `assert_hostname` and `cert_reqs` real connection attributes with default values.
2019-07-13 14:13:35 +02:00
Arthur Hamon
14d1454bbf test with latest urllib3 2019-07-13 14:11:59 +02:00
Jan Gazda
4daafcc687 Rename testing user 2019-07-13 12:51:50 +02:00
Jan Gazda
347026f42c Fix #382 - boto3 compatibility
* Add support for PyYaml5.1
* Unpin requests in Tox
* Unpin urllib3 in Tox
* Unpin Flask in Tox
* Add env vars to Tox for boto3 tests
2019-07-13 12:51:50 +02:00
Arthur Hamon
8297d65038 Merge pull request #455 from browniebroke/patch-1
Fix Python version for extra requirement YARL
2019-07-11 13:08:54 +02:00
Luiz Menezes
792c5c4558 Record intermediary redirect responses on aiohttp stub 2019-07-09 18:39:36 -03:00
Bruno Alla
2fa2ca2072 Revert "Remove yarl from extra dependencies: should be there if needed via aiohttp"
This reverts commit 26a4b8df
2019-07-08 14:39:21 +01:00
Bruno Alla
26a4b8df55 Remove yarl from extra dependencies: should be there if needed via aiohttp 2019-07-08 14:12:05 +01:00
Bruno Alla
41e174ccb6 Fix Python version for extra requirement YARL
This was excluded when Python 3.4 support was dropped in #435
2019-07-08 13:29:04 +01:00
Stefan T
a17624a464 Record json kwarg in aiohttp request (#412)
* Record `json` kwarg in aiohttp request

Aiohttp supports `data` and `json` parameters to make a request.
Aiohttp enforces that only one is used make a request.

* Log when aiohttp stub makes request to live server

resolves: #406

* Record aiohttp `auth` request kwarg

Aiohttp request supports basic auth via the `auth` method parameter.
It is set as a request header by aiohttp ClientRequest.
2019-07-05 10:57:39 +10:00
Luiz Menezes
bbab27ed1b Use primitive types when recording cassettes using aiohttp stub (#454) 2019-07-05 10:52:08 +10:00
Arthur Hamon
b92be4e9e5 Merge pull request #386 from valgur/unicode-match-on-body
Fix matching on 'body' failing when Unicode symbols are present
2019-07-02 12:33:21 +02:00
Martin Valgur
c6e7cb12c6 Merge branch 'master' into unicode-match-on-body 2019-07-01 22:09:57 +03:00
Arthur Hamon
d07915ccf6 Merge pull request #408 from StasEvseev/feature/mock_client_json_empty_body
FIX behaviour MockClientResponse.json() if body is empty
2019-07-01 20:38:34 +02:00
Stanislav Evseev
7c14d81ab1 CHANGE: return None from json if body is empty 2019-07-01 14:25:38 +02:00
Arthur Hamon
d682e7b19a Fix all warnings (#449)
* fix typo in pytest.mark.xskip

Change xskip by skipif marker as xskip is an unknown pytest marker.

* fix FileModeWarning

This fix the following warning:
FileModeWarning: Requests has determined the content-length for this request using the binary size of the file: however, the file has been opened in text mode (i.e. without the 'b' flag in the mode). This may lead to an incorrect content-length. In Requests 3.0, support will be removed for files in text mode.

* fix waring "calling yaml.load() without Loader=... is deprecated"
This fix the following warning:
YAMLLoadWarning: calling yaml.load() without Loader=... is deprecated, as the default Loader is unsafe. Please read https://msg.pyyaml.org/load for full details.

* fix collections.abc deprecation warning in python 3.7.

* update Flask dependency in order to get rid of the Request.is_xhr warning

This fix the following warning:
DeprecationWarning: 'Request.is_xhr' is deprecated as of version 0.13 and will be removed in version 1.0. The 'X-Requested-With' header is not standard and is unreliable. You may be able to use 'accept_mimetypes' instead.
2019-07-01 17:45:01 +10:00
Arthur Hamon
2b9498e009 Merge pull request #437 from steinnes/minimal-aiohttp-streamreader-support
Minimal aiohttp streamreader support
2019-07-01 07:16:54 +02:00
Arthur Hamon
eb99a3e36f Merge pull request #442 from addgene/master
fixes #440 VCR responses don't work with Biopython on Python3
2019-06-30 22:54:21 +02:00
Rishab Malik
6c877a1749 added skip decorator for python2 2019-06-28 13:03:55 -04:00
Arthur Hamon
6be6f0236b Merge pull request #415 from jeking3/issue-414
KeyError when replaying proxy tunnel captured sessions (broken in #389, v2.0.0)
2019-06-28 17:20:14 +02:00
Rishab Malik
95c7898b65 [DEV-5365][DEV-5367] VCR responses don't work with Biopython on Python 3 (#1)
* added IOBase methods and fp pointer

* regression test
2019-06-28 09:37:53 -04:00
James E. King III
d4b706334c properly handle tunnel connect uri generation broken in #389 2019-06-28 13:37:14 +00:00
Steinn Eldjárn Sigurðarson
e8a9a65bef Make test use stream output to test MockStream interface 2019-06-28 13:35:05 +00:00
Steinn Eldjárn Sigurðarson
67b03b45c3 Add output option to use response.content stream 2019-06-28 13:34:48 +00:00
Steinn Eldjárn Sigurðarson
6c41b8b723 Use built-in helpers to avoid use of async keywords which cause syntax errors on 2.x 2019-06-28 13:30:14 +00:00
Steinn Eldjárn Sigurðarson
cc55ef5b35 Add test for MockStream on aiohttp 2019-06-28 12:55:54 +00:00
Steinn Eldjárn Sigurðarson
1b6f304421 Add .content to MockClientResponse so code which uses aiohttp request content streams directly can be tested 2019-06-28 12:55:54 +00:00
Arthur Hamon
9039eab916 Merge pull request #418 from PiDelport/patch-1
Doc typo: yml -> yaml
2019-06-28 13:55:10 +02:00
Josh Peak
92e03603ea Merge pull request #446 from neozenith/master
CannotOverwritteCassetteException: Add properties cassette and failed_request
2019-06-28 13:54:43 +10:00
Josh Peak
f8e8b85790 Address testing with tox on windows documentation 2019-06-28 13:29:55 +10:00
Josh Peak
09ed0e911e Add cassette and failed request as properties of thrown CannotOverwriteCassetteException 2019-06-28 11:29:12 +10:00
Josh Peak
0830f6052b Merge pull request #439 from arthurHamon2/master
Add verbosity/explanations on CannotOverwriteExistingCassetteException
2019-06-28 10:45:10 +10:00
Arthur Hamon
829e9b2f1c Merge pull request #444 from hugovk/patch-1
Remove broken Waffle badge
2019-06-27 22:30:10 +02:00
Arthur Hamon
b203fd4113 add reference to pytest-vcr plugin in the documentation 2019-06-27 22:22:10 +02:00
Arthur Hamon
f414e04f49 update the documentation of custom matchers
Add documentation on creating a matcher with an `assert` statement that provides assertion messages in case of failures.
2019-06-27 22:22:10 +02:00
Arthur Hamon
28d9899b9b refactor the 'CannotOverwriteExistingCassetteException' exception, building a more detailed message
The 'CannotOverwriteExistingCassetteException' exception now takes two kwargs, cassette and failed requests, in order to get the request(s) in the cassettes with the less differences and put those details in the exception message.
2019-06-27 22:22:10 +02:00
Arthur Hamon
396c4354e8 add cassette's method to find the most similar request(s) of a request
This method get the requests in the cassettes with the most matchers that succeeds.
2019-06-27 22:22:10 +02:00
Arthur Hamon
0a01f0fb51 change all the matchers with an assert statement and refactor the requests_match function
In order to use the new assert mechanism that returns explicit assertion failure message, all the default matchers does not return a boolean, but only do an assert statement with a basic assertion message (value_1 != value_2).
The requests_match function has been refactored to use the 'get_matchers_results' function in order to have explicit failures that are logged if any.
Many unit tests have been changed as the matchers does not return a boolean value anymore.
Note: Only the matchers "body" and "raw_body" does not have an assertion message, the body values might be big and not useful to be display to spot the differences.
2019-06-27 22:22:10 +02:00
Arthur Hamon
46f5b8a187 add function to get the comparaison result of two request with a list of matchers
The function returns two list:
- the first one is the list of matchers names that have succeeded.
- the second is a list of tuples with the failed matchers names and the related assertion message like this ("matcher_name", "assertion_message").
If the second list is empty, it means that all the matchers have passed.
2019-06-27 22:22:10 +02:00
Arthur Hamon
940dec1dd6 add private function to evaluate a matcher
A matcher can now return other results than a boolean :
- An AssertionError exception meaning that the matcher failed, with the exception we get the assertion failure message.
- None, in case we do an assert in the matcher, meaning that the assertion has passed, the matcher is considered as a success then.
- Boolean that indicates if a matcher failed or not. If there is no match, a boolean does not give any clue what it is the differences compared to the assertion.
2019-06-27 22:22:10 +02:00
Arthur Hamon
de244a968f add function to format the assertion message
This function is used to prettify the assertion message when a matcher failed and return an assertion error.
2019-06-27 22:22:10 +02:00
Arthur Hamon
728dc71a35 Merge pull request #441 from arthurHamon2/update-related-to-new-dependencies-versions
Update tests related to new dependencies versions
2019-06-27 22:03:30 +02:00
Arthur Hamon
bdb74b9841 change requests27 in travis-ci to requests 2019-06-27 21:33:39 +02:00
Arthur Hamon
34f0417dc9 add troubleshooting in contributing section
When I ran the test suite on macOSX, I had some issues regarding SSL configuration, I have documented the error I encounter and the solution to deal with it.
2019-06-27 21:33:39 +02:00
Hugo
86586e8cd9 Remove broken Waffle badge
Waffle shut down on May 16th, 2019 "due to market direction and the acquisition by Broadcom".

https://help.waffle.io/articles/2801857-waffle-shutdown-tl-dr
2019-06-17 11:48:12 +03:00
Arthur Hamon
7724b364aa urllib3 has a new default behavior with PoolManager object creation
By default, from version urllib3=>1.25, urllib3 requires and validates certificates by default when using HTTPS (https://github.com/urllib3/urllib3/blob/master/CHANGES.rst#125-2019-04-22).
Set explicitly that we deactivate certificates validation with cert_reqs=`CERT_NONE`.
2019-06-13 16:04:26 +02:00
Arthur Hamon
c4803dbc4d httplib2 has issues validating certificate with python 2.7, disabling it 2019-06-13 16:04:26 +02:00
Arthur Hamon
a53121b645 do not create tox environment with python2 and aiohttp
aiohttp dependency only works with python 3.5+
2019-06-13 16:04:26 +02:00
Arthur Hamon
78a0a52bd9 fix issue with tests using localhost
The error we have : SSLError: hostname '127.0.0.1' doesn't match either of 'localhost', '127.0.0.1'
This is fixed by adding the `ipaddress` dependency in the tox ini.
2019-06-13 16:04:26 +02:00
Ivan Malison
18977a85d1 Merge pull request #413 from khamidou/add_rewind_method
Add a `rewind` method to reset a cassette.
2019-06-10 17:53:37 -07:00
Luiz Menezes
fb84928ef6 Fix build problems on requests tests due to SSL certificate problems 2019-06-10 16:57:24 -03:00
Luiz Menezes
0b4d92c277 Merge pull request #435 from hugovk/rm-3.4
Drop support for EOL Python 3.4
2019-06-10 15:38:36 -03:00
Hugo
8f4e089200 Upgrade Python syntax with pyupgrade 2019-05-22 17:44:36 +03:00
Hugo
7670e10bc2 Fix Flake8: E117 over-indented 2019-05-22 17:35:15 +03:00
Hugo
dc174c3250 Drop support for EOL Python 3.4 2019-05-22 17:30:45 +03:00
Luiz Menezes
114fcd29b4 Merge pull request #416 from davidwilemski/util-warnings
Fix collections.abc DeprecationWarning
2019-04-06 00:17:32 -03:00
Pi Delport
20e8f4ad41 Doc typo: yml -> yaml 2019-02-04 12:15:23 +02:00
David Wilemski
4e990db32e Fix collections.abc DeprecationWarning
In versions of Python from 3.8 and forward, importing Mapping and
MutableMapping from the collections module will no longer work. This
change will try to import from the collections.abc module, which was
added in Python 3.3, and fall back to the collections module on older
versions of Python.
2019-01-01 15:23:51 -08:00
Karim Hamidou
472bc3aea1 Add a rewind method to reset a cassette. 2018-12-20 12:44:07 +01:00
Luiz Menezes
c74a857aa4 Merge pull request #409 from jxltom/pin-requires-version-for-python34
Pin yarl and multidict version for python34
2018-11-14 09:59:39 -03:00
jxltom
c3705dae9f Fix flake8 in python3 2018-11-14 10:27:38 +08:00
jxltom
6c166482d9 Pin yarl and multidict version for python34 2018-11-14 09:48:49 +08:00
Thomas Grainger
cc9fabf2d9 Merge pull request #379 from adamchainz/patch-1
Update docs on before_record_* callbacks
2018-10-15 04:05:16 +01:00
Thomas Grainger
f77442d87b Merge pull request #400 from felixonmars/patch-1
Fix a typo in vcr/request.py
2018-10-15 04:04:32 +01:00
Thomas Grainger
602112cd87 Merge pull request #404 from kevin1024/text-encoding-errors-kwarg
support ClientResponse.text(errors=) kwarg
2018-10-15 04:04:15 +01:00
Thomas Grainger
4ef5205094 support ClientResponse.text(errors=) kwarg 2018-10-15 03:00:19 +01:00
Felix Yan
0d2f49fe8a Fix a typo in vcr/request.py 2018-10-06 17:21:42 +08:00
Luiz Menezes
8fdc6dbb68 Merge pull request #397 from stj/master
ClientResponse.release isn't a coroutine
2018-10-04 12:04:05 -03:00
Stefan Tjarks
ffc4dca502 ClientResponse.release isn't a coroutine
Therefore it should not be one in the MockClientResponse class.

d0af887e31/aiohttp/client_reqrep.py (L832)
2018-09-26 00:30:11 -07:00
Luiz Menezes
e42746fa88 Bump version to 2.0.1 2018-09-23 15:25:04 -03:00
Luiz Menezes
03b1dd9faa Merge pull request #395 from kevin1024/fix-py34
Fix py34
2018-09-22 23:16:14 -03:00
Luiz Menezes
f2a79d3fcc Add py34 to CI builds 2018-09-22 17:30:21 -03:00
Luiz Menezes
287ea4b06e Fix cassette module to work with py34 2018-09-22 17:29:08 -03:00
Luiz Menezes
0cf11d4525 Bump version to 2.0.0 2018-09-19 11:46:08 -03:00
Luiz Menezes
75a334686f Merge pull request #391 from kevin1024/py37
Add support to py37
2018-09-18 19:40:45 -03:00
Luiz Menezes
9a9cdb3a95 add py37 on CI build 2018-09-18 19:04:30 -03:00
Luiz Menezes
b38915a89a Fix httplib2 compatibility with py37 2018-09-18 18:42:22 -03:00
Felix Yan
e93060c81b Fix compatibility with Python 3.7 2018-09-18 16:41:42 -03:00
Luiz Menezes
10736db427 Merge pull request #374 from adamchainz/readme_supported
Update docs' lists of supported HTTP clients
2018-09-18 14:45:11 -03:00
Luiz Menezes
cb4228cf90 Merge pull request #390 from kevin1024/fix-aiohttp-client
Fix vcr to support aiohttp client requests
2018-09-18 14:42:59 -03:00
Luiz Menezes
f7c051cde6 Drop support to asyncio.coroutine (py34 async/await syntax) 2018-09-18 14:14:25 -03:00
Luiz Menezes
075dde6707 Merge pull request #384 from valgur/patch-1
Fix PyPI badge URLs in README.rst
2018-09-18 12:55:32 -03:00
Luiz Menezes
af2742b6b9 Merge pull request #389 from lamenezes/fix-proxy
Fix proxy
2018-09-18 11:32:53 -03:00
Luiz Menezes
e9d00a5e2a Fix test_use_proxy cassette response type 2018-09-18 11:13:39 -03:00
Danilo Shiga
957db22d5c Improve test_use_proxy with cassette headers and play_count assertion 2018-09-18 11:06:15 -03:00
Martin Valgur
302ea35d9a Fix matching on 'body' failing when Unicode symbols are present in them
Matching on bodies uses urllib.parse.parse_qs(), which fails to handle
UTF-8+URLEncoded POST bodies when the input is `bytes` rather than `str`,
causing matching to fail..
Fixed this by always doing decode('ascii') on URLEncoded POST bodies first.
2018-09-06 19:41:37 +03:00
Martin Valgur
895850b197 Fix failing unit test on Windows
test_xmlrpclib was failing with "can't pickle thread.lock objects" on Windows.
Other small issues were related to backslashes in paths and different line endings.
2018-09-06 19:41:37 +03:00
Martin Valgur
5ddcaa4870 Replace PNG badges with SVG ones 2018-09-05 20:26:01 +03:00
Martin Valgur
76076e5ccb Fix PyPI badge URLs in README.rst
`vcrpy-unittest` -> `vcrpy` in PyPI URLs.
2018-09-05 20:05:27 +03:00
Adam Johnson
7417978e36 tornado.httpclient 2018-07-31 10:38:21 +01:00
Adam Johnson
a9e75a545e Update docs on before_record_* callbacks
Make them a bit more consistent and obvious that returning `None` ignores the request/response.
2018-07-31 09:54:29 +01:00
Stefan Tjarks
e559be758a Fix aiohttp patch to work with aiohttp >= 3.3
Aiohttp expects an awaitable instance to be returned from
`ClientSession._request` though `asyncio.coroutine` decorated function
do not implement `__await__`. By changing the syntax and dropping Python
3.4 support we fix this issue.
2018-07-25 13:24:29 -03:00
Adam Johnson
bb8d39dd20 Update docs' lists of supported HTTP clients
I noticed these were out of sync, especially weirdly one mentioning boto and the other mentioning boto3. I figure the list in the README is redundant when the Installation docs section lists the supported libraries, so I've removed it. I also:

* alphabetically sorted the list
* Highlighted the library names as code
* Added both `boto` and `boto3` to the list since there is support for both
* Removed the comment about Tornado's AsyncHTTPClient since that's an implementation detail plus the patch happens on a couple different classes
* Removed the note about `http.client` being part of Python 3, because everyone is Python 3 these days anyway :)
2018-07-18 22:51:58 +01:00
Samuel Fekete
ff7dd06f47 fix proxy for Python 2 2018-07-13 16:44:19 +01:00
Samuel Fekete
4b6b5effc7 Add headers in proxy server response 2018-07-13 16:44:19 +01:00
Samuel Fekete
06dc2190d6 Fix format string for Python 2.6 2018-07-13 16:40:35 +01:00
Samuel Fekete
eb4774a7d2 Only have a sock attribute after connecting 2018-07-13 16:40:35 +01:00
Samuel Fekete
bed9e520a3 Fix socketserver for Python 3 2018-07-13 16:40:35 +01:00
Samuel Fekete
365a98bf66 Fix failing tests 2018-07-13 16:40:35 +01:00
Samuel Fekete
fc95e34bd4 Determine proxy based on path 2018-07-13 16:40:35 +01:00
Samuel Fekete
236dc1f4f2 Add test for proxies 2018-07-13 16:34:21 +01:00
Samuel Fekete
43f4eb8156 Fix host and port for proxy connections 2018-07-13 16:34:21 +01:00
Luiz Menezes
aff71c5107 Merge pull request #356 from kgraves/fix_before_record_response_mutates_actual_response
fixes before_record_response mutates response
2018-07-12 23:19:57 -03:00
Luiz Menezes
506700651d Bump version to 1.13.0 2018-07-12 20:07:30 -03:00
Luiz Menezes
d1b11da610 Merge pull request #369 from lamenezes/fix-aiohttp-url-with-query-params
Fix aiohttp url with query params
2018-07-08 23:48:20 -03:00
Luiz Menezes
306238d561 Test aiohttp usage with query strings on the URL 2018-07-08 23:06:22 -03:00
Goran Stefkovski
dbddaa0e44 Shallow copy of query as mutable MultiDict 2018-07-08 23:04:36 -03:00
Goran Stefkovski
0d4c9eccf5 simplified logic so that either params or url is used, if params are specified - they will overwrite any get params on the url 2018-07-08 23:03:42 -03:00
Luiz Menezes
1674741d9f Merge pull request #368 from lamenezes/fix-json-content-type-on-aiohttp-stub
Fix content type being passed to aiohttp response stub
2018-07-08 14:46:16 -03:00
Luiz Menezes
75cb067e29 Fix content type being passed to aiohttp response stub 2018-07-07 23:56:39 -03:00
Luiz Menezes
ab6e6b5b5d Merge pull request #359 from lamenezes/adapt-aiohttp-stub-to-3.3
Fix aiohttp stub to work with aiohttp 3.3.x
2018-06-18 10:46:36 -03:00
Luiz Menezes
9e8bd382d3 Fix aiohttp stub to work with aiohttp 3.3.x 2018-06-05 11:59:28 -03:00
kg
ba79174a1f fixes before_record_response mutates response
When no cassette exists, it's expected that the response returned, should be
the original, unchanged response. The response recorded in the cassette should be
that which is returned by the before_record_response callback.

But on subsequent requests/responses (when a cassette exists), the responses
returned should be exactly what is in the cassette.

resolves #355
2018-05-24 01:12:13 +00:00
Kevin McCarthy
c341e48961 bump version for release 2018-05-21 08:57:42 -05:00
Luiz Menezes
5be75692c4 Merge pull request #353 from lamenezes/support-aiohttp-over-3.1.0
Fix aiohttp stub to support version >= 3.1.0
2018-05-16 15:13:37 -03:00
Luiz Menezes
b10b92bdbb Fix travis config to allow pypy3.5-aiohttp failure instead of excluding 2018-05-16 14:09:39 -03:00
Luiz Menezes
3009cbbbe9 Skip requests test instead of failing for python >= 3.6 2018-05-16 12:39:44 -03:00
Luiz Menezes
f811b41ad9 Fix tests aiohttp_utils.aiohttp_request return values 2018-05-16 11:12:51 -03:00
Luiz Menezes
140bc2ee74 Allow test failure on pypy3+aiohttp 2018-05-08 09:45:17 -03:00
Luiz Menezes
867fd9ab4b Ignore flake8 for entire file on aiohttp_utils 2018-05-07 10:58:09 -03:00
Luiz Menezes
545c903ee2 pep8 fix on aiohttp_utils 2018-05-07 10:00:27 -03:00
Luiz Menezes
cd864b5eca Fix aiohttp tests to include content type when parsing response to json 2018-05-07 09:55:12 -03:00
Luiz Menezes
689d68a0a2 Use async syntax on aiohttp_utils 2018-05-07 09:25:21 -03:00
Luiz Menezes
709017ea46 Fix aiohttp utils to pass encondig to response.json 2018-05-07 09:25:21 -03:00
Luiz Menezes
8621427f46 Add .pytest_cache/ to .gitignore 2018-05-07 09:25:21 -03:00
Luiz Menezes
7e695ff7bc Fix test aiohttp imports 2018-05-07 09:25:21 -03:00
Luiz Menezes
bd08e5119f Maintain support to python 3.4 and aiohttp 2018-05-07 09:25:21 -03:00
Luiz Menezes
6ab508d67d Fix aiohttp_request to properly perform aiohttp requests 2018-05-07 09:25:21 -03:00
Luiz Menezes
f1561ae0f8 Remove tox pin on aiohttp 2018-05-07 09:25:21 -03:00
Luiz Menezes
f1f8ce2af4 Ignore syntax error on async stuff 2018-05-07 09:25:21 -03:00
Luiz Menezes
26be756f47 Fix aiohttp stub to support version >= 3.1.0 2018-05-07 09:25:21 -03:00
Luiz Menezes
f890709a20 Merge pull request #346 from carsonyl/patch-1
Convert extras_require conditional deps to PEP 508 form.
2018-05-06 19:06:06 -03:00
Carson Lam
d0ae5fa40b Merge branch 'master' of github.com:kevin1024/vcrpy into patch-1 2018-05-05 19:53:42 -07:00
Luiz Menezes
1562bc7659 Merge pull request #350 from lamby/895269-vcrpy-please-make-the-build-reproducible
Make the build reproducible
2018-05-03 12:31:29 -03:00
Luiz Menezes
16b69aa2e5 Merge pull request #320 from allisson/master
Update aiohttp_stub to work with binary content
2018-05-03 12:30:41 -03:00
Allisson Azevedo
d9caff107d Merge remote-tracking branch 'upstream/master' 2018-05-02 15:03:01 -03:00
Chris Lamb
f317490eec Make the build reproducible
Whilst working on the Reproducible Builds effort [0], we noticed
that vcrpy could not be built reproducibly.

This is due to the documentation including the absolute build path
via Python default arguments.

This was originally filed in Debian as #895269 [1].

 [0] https://reproducible-builds.org/
 [1] https://bugs.debian.org/895269

Signed-off-by: Chris Lamb <lamby@debian.org>
2018-05-02 10:14:43 -07:00
Luiz Menezes
cf13805973 Merge pull request #354 from kevin1024/build-quicker
Fix broken tests + build quicker
2018-05-02 13:31:36 -03:00
Luiz Menezes
389cb4d6e3 Temporarily pins aiohttp to version <3 2018-05-02 12:19:55 -03:00
Luiz Menezes
7a82d70391 Pin flask version (as latests 1.0 breaks retrocompatibility) 2018-05-02 11:59:01 -03:00
Carson Lam
f3b9966a2a Pass flake8. 2018-02-21 23:16:29 -08:00
Carson Lam
5ba1c7fbb6 Convert extras_require conditional deps to PEP 508 form. 2018-02-21 21:40:02 -08:00
Allisson Azevedo
ad153bd733 Merge remote-tracking branch 'upstream/master' 2018-02-17 11:51:04 -03:00
Hugo
42b3b16fe1 Remove boto 2018-01-19 23:56:06 +02:00
Hugo
531dc02ca5 Only test on single recent version of dependencies 2018-01-17 10:28:36 +02:00
Thomas Grainger
2156adb841 Merge pull request #340 from hugovk/rm-2.6
Drop support for EOL Python 2.6 and 3.3
2018-01-16 17:23:12 +00:00
Hugo
6caf7e962e Requests 1.x only supports Python <= 3.3 2018-01-15 22:37:27 +02:00
Hugo
97fbd7e0bd Test flakes on Python 2.7 (and 3.6) not pypy3.5 2018-01-15 22:36:19 +02:00
Hugo
ead48b1907 xfail for test_post_chunked_binary_secure on CPython 3.5 2018-01-15 15:05:53 +02:00
Hugo
1af4b2587e Add pypy3.5-5.9.0 2018-01-15 10:04:25 +02:00
Hugo
82fa50c092 Drop pypy3 2018-01-15 09:54:39 +02:00
Hugo
58d8980cfa Remove dependency on bugs.python.org/xmlrpc: 301 Moved Permanently 2018-01-15 09:54:11 +02:00
Hugo
c111ebab0a Remove dependency on old Flask 2018-01-15 09:54:11 +02:00
Hugo
943a15a967 Drop support for EOL Python 3.3 2018-01-15 09:54:11 +02:00
Hugo
d0aa6bcc8d Rewrite unnecessary list/tuple literals as set literals 2018-01-15 09:54:11 +02:00
Hugo
04fd730a08 Replace unnecessary list comprehension - 'all' can take a generator 2018-01-15 09:54:11 +02:00
Hugo
6156271c48 Automatic formatters supported in Python 2.7+ 2018-01-15 09:54:11 +02:00
Hugo
87666ba2e4 Multiple context managers supported in 2.7+ 2018-01-15 09:54:11 +02:00
Hugo
7915d07aff Update classifiers 2018-01-15 09:54:11 +02:00
Hugo
095e272191 Add python_requires to help pip install correct version 2018-01-15 09:54:11 +02:00
Hugo
42762ec806 collections.Counter is new in Python 2.7 2018-01-15 09:54:11 +02:00
Hugo
bfb38af8e1 Drop support for EOL Python 2.6 2018-01-15 09:54:11 +02:00
Hugo
894695d13b Stop testing on EOL Python 2.6 2018-01-15 09:54:11 +02:00
Andrew Kofink
a56a0726d4 Correct libyaml-devel package name in EL/Fedora 2017-10-30 10:20:56 -10:00
Ivan Malison
c366852925 Merge pull request #324 from petr-bulusek/custom-persister
Fix of registering custom persister and test.
2017-10-25 02:17:08 -07:00
Allisson Azevedo
0cab15658f Merge remote-tracking branch 'upstream/master' 2017-09-20 11:58:07 -03:00
Ivan Malison
c3ecf8c5b2 Merge pull request #326 from lmazuel/master
Fix mixup between httplib and urllib3
2017-08-17 10:08:38 -07:00
Laurent Mazuel
81d453f7d3 Add requests 2.18.4 to build 2017-08-17 08:37:11 -07:00
Laurent Mazuel
262ad903cb Don't unmock requests.packages if not necessary 2017-08-17 08:37:11 -07:00
Laurent Mazuel
ec60af0214 Fix mixup between httplib and urllib3 2017-08-17 08:37:11 -07:00
Petr Bulusek
8cf8d3f69c delete ipdb.set_trace 2017-08-12 17:58:52 +02:00
Petr Bulusek
034aeb4f17 typo 2017-08-11 14:41:11 +02:00
Petr Bulusek
d59efbc6e0 Instantiating class not necessary. 2017-08-09 15:18:00 +02:00
Petr Bulusek
b753a491c9 revert PR unrelated change, better comment 2017-08-09 14:44:39 +02:00
Petr Bulusek
9092b34dd1 register custom persister fix 2017-08-09 14:25:10 +02:00
Mikaeil Orfanian
0a3aaddca2 Update usage.rst
"i.e." is misleading because it means "in other words". In the context of this paragraph, "i.e." implies that "unexpected requests" are those that have a different URI.
I think a URI change is just one example of what could constitute an "unexpected" request. vcr does request matching based on method, port, body, etc.
So, I think "e.g." which means "for example" is the right phrase in this context.
2017-07-13 17:09:34 -05:00
Allisson Azevedo
c55d976277 Update aiohttp_stub to work with binary content 2017-06-22 15:12:58 -03:00
Ivan Malison
47ccddafee Merge pull request #314 from kevin1024/support-requests211
support requests 2.11 Fixes #313
2017-06-07 11:27:26 -07:00
Thomas Grainger
dcaf813657 support requests 2.11 Fixes #313 2017-06-07 14:10:23 +01:00
Kevin McCarthy
ef727aaaaf bump version 2017-05-27 16:47:11 -10:00
Kevin McCarthy
ee17233aa0 only need to run flakes on one version of python 2017-05-27 16:00:11 -10:00
Kevin McCarthy
f88294a9e6 Drop a couple old versions of python
According to the devguide: https://docs.python.org/devguide/

Python 3.3 is officially supported until 2017-09-29 and 3.4 until
2019-03-16

But I'm tired of waiting for my tests to run on Travis :-P
2017-05-27 15:52:59 -10:00
Kevin McCarthy
572da2084d Fix pyflake issue 2017-05-27 15:43:24 -10:00
Kevin McCarthy
88bf8f0aac proxy getattr to the real connection too 2017-05-27 15:40:42 -10:00
Kevin McCarthy
9b59e02374 Fix compat with requests 2.16 (unvendored urllib3)
The new version of requests un-vendors urllib3, with a nifty hack:
https://github.com/kennethreitz/requests/blob/master/requests/packages.py

Unfortunately messing directly with sys.modules causes some weird
behavior that I don't entirely understand.  Avoiding the extra import to
requests.packages as part of VCR's initialization seems to sidestep the
issue.

Closes #311
2017-05-27 15:40:38 -10:00
Kevin McCarthy
ba290a32d2 Add new version of urllib3 to tox
Also remove ancient version in the interest of test speed
2017-05-27 15:38:42 -10:00
Kevin McCarthy
420c2ceb6f add new requests version 2017-05-27 10:57:41 -10:00
Kevin McCarthy
ec786f2fd9 pass all args to runtests 2017-05-27 10:57:25 -10:00
Thomas Grainger
0c4020df7d version bump 1.11.0 2017-05-02 11:36:16 +01:00
Thomas Grainger
204cb8f2ac Merge pull request #303 from graingert/support-3.6
support 3.6
2017-05-02 11:17:39 +01:00
Thomas Grainger
0e421b5327 Merge pull request #301 from graingert/handle-pytest-asyncio-coroutines
handle pytest-asyncio async def coroutines
2017-05-02 11:17:06 +01:00
Thomas Grainger
dc2dc306d5 pytest-httpbin doesn't support chunked requests on Python 3.6 2017-04-04 11:30:16 +01:00
Thomas Grainger
1092bcd1a1 add requests 2.13 2017-04-04 10:32:17 +01:00
Thomas Grainger
73dbc6f8cb add missing _get_content_length static method
also add _is_textIO
2017-04-04 10:32:17 +01:00
Thomas Grainger
3e9fb10c11 Merge remote-tracking branch 'derekbekoe/fix-vcrconnection-3.6' into support-3.6 2017-04-04 10:32:17 +01:00
Thomas Grainger
3588ed6341 support 3.6 2017-04-04 10:32:16 +01:00
Kevin McCarthy
26326c3ef0 Merge pull request #305 from graingert/add-cache-to-gitignore
add .cache to gitignore
2017-04-03 06:40:21 -10:00
Thomas Grainger
7514d94262 handle pytest-asyncio async def coroutines 2017-04-03 15:49:49 +01:00
Thomas Grainger
1df577f0fc add .cache to gitignore 2017-04-03 15:45:42 +01:00
Kevin McCarthy
70f4707063 Merge pull request #302 from AartGoossens/feature/before_record_docs_fix
Improves docs for before_record_request
2017-03-16 08:55:34 -10:00
Aart Goossens
521146d64e Improves docs for before_record_request 2017-03-16 19:25:16 +01:00
Derek Bekoe
091b402594 Revert "Add Python 3.6 to CI"
This reverts commit 24b617a427.
2017-01-24 16:26:02 -08:00
Derek Bekoe
24b617a427 Add Python 3.6 to CI 2017-01-24 13:57:37 -08:00
Derek Bekoe
97473bb8d8 Correctly patch HTTPConnection.request in Python 3.6
Fixes https://github.com/kevin1024/vcrpy/issues/293
2017-01-23 14:54:26 -08:00
Kevin McCarthy
ed35643c3e Merge pull request #292 from j-funk/master
Allow injection of persistence methods
2017-01-22 08:29:52 -06:00
Julien Funk
2fb3b52c7e add custom persister docs 2017-01-19 13:10:27 -05:00
Julien Funk
9e70993d57 substiture IOError with more appropriate ValueError 2017-01-19 13:10:08 -05:00
Julien Funk
6887e2cff9 remove unused imports 2017-01-15 15:04:20 -08:00
Julien Funk
ba38680402 Merge pull request #1 from IvanMalison/persistence_methods
Fix patch of FliesystemPersister.load_cassette
2017-01-14 15:59:13 -05:00
Ivan Malison
06b00837fc Fix patch of FliesystemPersister.load_cassette 2017-01-13 15:29:45 -08:00
Julien Funk
a033bc729c refactored, 1 failing test 2017-01-13 16:09:42 -05:00
Julien Funk
6f8486e0a2 allow injection of persistence methods 2017-01-12 16:41:26 -05:00
Kevin McCarthy
53c55b13e7 version bump 2017-01-11 17:54:16 -10:00
MAA
365e7cb112 Removed duplicate mock triple. 2017-01-11 17:54:15 -10:00
Charly
e5d6327de9 added a fix to httplib2 2017-01-11 17:54:15 -10:00
Luiz Menezes
d86ffe7130 Add missing requirement yarl for python >= 3.4 2017-01-06 10:43:40 -02:00
Kevin McCarthy
d9fd563812 bump version 2016-12-15 08:47:01 -10:00
Kevin McCarthy
9e548718e5 fix whitespace 2016-12-15 08:47:01 -10:00
Kevin McCarthy
83720793fb Merge pull request #280 from madninja/fix_aiohttp
Fix up to support aiohttp 1.x
2016-11-08 10:15:07 -10:00
Marc Nijdam
188326b10e Fix flake errors 2016-11-07 12:03:21 -08:00
Marc Nijdam
ff90190660 Fix up to support aiohttp 1.x 2016-11-07 10:07:08 -08:00
Kevin McCarthy
1d9f8b5f7c bump version to 1.10.3 2016-10-02 12:15:12 -10:00
Kevin McCarthy
2454aa2eb0 Merge pull request #278 from kevin1024/empty_response_body
Empty response body
2016-10-02 12:10:04 -10:00
Kevin McCarthy
df5f6089af Merge pull request #279 from kevin1024/fix_nonetype_encode_exception
Fix nonetype encode exception
2016-10-02 12:09:55 -10:00
Kevin McCarthy
5738547288 Merge pull request #277 from kevin1024/fix_asyncio
Fix asyncio
2016-10-02 12:09:48 -10:00
Kevin McCarthy
8274b660c6 fix flake8 failure 2016-10-02 12:08:02 -10:00
Gregory Roussac
a8f1a65d62 test serializers.compat.convert_to_bytes() 2016-10-02 12:07:37 -10:00
Gregory Roussac
9c275dd86a VCR AttributeError: 'NoneType' object has no attribute 'encode'
Hi,

Using an old fork but may be usefull.

I think checking the string is required like on line 47.

Best regards
2016-10-02 12:07:37 -10:00
Kevin McCarthy
1fbd65a702 add test from @mbachry 2016-10-02 10:24:38 -10:00
Janez Troha
31b0e825b5 Handle empty body 2016-10-02 10:24:38 -10:00
Luiz Menezes
973d8339b3 add tests for aiohttp params fix 2016-10-02 10:22:29 -10:00
Alexander Novikov
c8db6cb731 Fix missing query string while params are passed in inside params argument 2016-10-02 10:22:29 -10:00
Kevin McCarthy
ecbc192fc4 bump version 2016-09-13 15:49:18 -10:00
Kevin McCarthy
76d365314a bump version 2016-09-11 18:02:37 -10:00
Kevin McCarthy
830a3c2e04 Merge pull request #272 from puiterwijk/fix-270
Move vcr.stubs.aiohttp_stub to a package
2016-09-09 11:54:08 -10:00
Patrick Uiterwijk
9c432c7e50 Move vcr.stubs.aiohttp_stub to a package
find_packages(exclude=) only works with packages, not modules.
So this fixes install_lib for python2 by correctly excluding that module.

Fixes: #270
Fixes: #271
Signed-off-by: Patrick Uiterwijk <puiterwijk@redhat.com>
2016-09-09 20:45:16 +00:00
Kevin McCarthy
6f7f45d0a8 Merge pull request #271 from lamenezes/fix-py2-setup
Exclude aiohttp from python < 3 setup
2016-09-09 06:38:10 -10:00
Luiz Menezes
8e352feb6a Exclude aiohttp from python < 3 setup 2016-08-31 22:15:24 -03:00
Kevin McCarthy
57a934d14b version bump to 1.10.0 2016-08-14 10:41:25 -10:00
Kevin McCarthy
f9d7ccd33e Merge pull request #266 from lamenezes/aiohttp-support
Aiohttp support
2016-08-14 10:37:14 -10:00
Luiz Menezes
265a158fe7 remove py26-flakes test 2016-08-12 13:50:29 -03:00
Luiz Menezes
c65ff0e7b3 fix flake8: ignore yield from syntax errors 2016-08-11 19:48:40 -03:00
Luiz Menezes
066752aa0b rename file 2016-08-11 08:32:47 -03:00
Luiz Menezes
9a5214888b fix tox's flakes tests 2016-08-11 00:58:18 -03:00
Luiz Menezes
609d8e35be fix test_aiohttp 2016-08-10 18:12:38 -03:00
Luiz Menezes
ce14de8251 fix tests 2016-08-10 15:56:19 -03:00
Luiz Menezes
574b22a62a remove async/await from aiohttp_stubs to support python 3.4 2016-08-10 15:51:11 -03:00
Luiz Menezes
1167b9ea4e fix .travis.yml 2016-08-04 14:03:42 -03:00
Luiz Menezes
77ae99bfda add aiohttp to tests config 2016-08-04 13:44:11 -03:00
Luiz Menezes
8851571ba7 add integration tests for aiohttp 2016-08-04 13:40:04 -03:00
Luiz Menezes
f71d28d10e fix aiohttp_stubs.vcr_request error message 2016-08-04 13:39:46 -03:00
Luiz Menezes
3355bd01eb fix aiohttp response closing 2016-08-04 13:39:09 -03:00
Luiz Menezes
17afa82bf4 remove CIMultiDictProxy from aiohttp_stubs.vcr_request 2016-08-04 13:37:55 -03:00
Luiz Menezes
f98684e8aa add support for aiohttp 2016-08-04 00:21:49 -03:00
Kevin McCarthy
5a85e88a39 Merge pull request #265 from adamchainz/readthedocs.io
Convert readthedocs links for their .org -> .io migration for hosted projects
2016-07-16 09:08:29 -10:00
Kevin McCarthy
d2368eb2c4 fix flaky test 2016-07-16 08:58:07 -10:00
Kevin McCarthy
3a46616ba6 bump version to 1.9.0 2016-07-16 08:07:51 -10:00
Adam Chainz
37665581e0 Convert readthedocs links for their .org -> .io migration for hosted projects
As per [their blog post of the 27th April](https://blog.readthedocs.com/securing-subdomains/) ‘Securing subdomains’:

> Starting today, Read the Docs will start hosting projects from subdomains on the domain readthedocs.io, instead of on readthedocs.org. This change addresses some security concerns around site cookies while hosting user generated data on the same domain as our dashboard.

Test Plan: Manually visited all the links I’ve modified.
2016-07-13 23:24:04 +01:00
Kevin McCarthy
57df0c6921 unzip bodies before comparing. Fixes #261 2016-07-03 17:27:52 -10:00
Kevin McCarthy
ddb29745a9 Merge pull request #247 from dedsm/boto3_support
Adding support for boto3
2016-07-02 14:00:02 -10:00
David de Sousa
ac7c9244cc Running boto3 tests in travis 2016-06-27 01:39:49 +02:00
David de Sousa
6da7cd0ea5 Fixing pep8 errors in boto tests 2016-06-27 01:39:49 +02:00
bogdan barna
24df79b75f boto3 integration tests 2016-06-27 01:39:49 +02:00
David de Sousa
0800b99214 Adding support for boto3 2016-06-27 01:39:49 +02:00
Kevin McCarthy
3dad89df3f Merge pull request #260 from foobarna/fix-tests
fix tests in stubs, requests ssl verification and httpbin+flask
2016-06-24 06:48:51 -10:00
bogdan barna
5c9b0b4ccb fix tests in stubs, requests ssl verification and httpbin+flask 2016-06-24 11:43:01 +03:00
Kevin McCarthy
5a848d277e Merge pull request #259 from nickdirienzo/master
Fix deepcopy issue for response headers when `decode_compressed_response` is enabled
2016-06-21 20:45:27 -10:00
Nick DiRienzo
c88c738df9 Removed requests usage from test 2016-06-21 07:14:51 -07:00
Nick DiRienzo
9a8067d8e7 Renamed inside2 to inside 2016-06-21 07:00:53 -07:00
Nick DiRienzo
787c6bdb77 Fix flake8 issue 2016-06-21 06:54:05 -07:00
Nick DiRienzo
c3298c25a3 Updated comments 2016-06-20 23:43:47 -07:00
Nick DiRienzo
2f4c803678 Added a note on the deepcopy 2016-06-20 23:36:51 -07:00
Nick DiRienzo
60145983bf Added regression test 2016-06-20 23:23:31 -07:00
Nick DiRienzo
b5c27f99d1 Move deepcopy higher to not mutate original headers 2016-06-20 16:57:40 -07:00
Ivan Malison
1ef099a13e v 1.8.0 2016-06-09 12:16:14 -07:00
Kevin McCarthy
34d07406f9 missed a httpbin call 2016-05-01 17:16:05 -10:00
Kevin McCarthy
e269c77670 Merge pull request #253 from jayvdb/no-pytest-localserver
Remove pytest-localserver from test dependencies
2016-05-01 16:43:09 -10:00
John Vandenberg
889edccecb Remove pytest-localserver from test dependencies
pytest-localserver is no longer needed after
the switch to using pytest-httpbin.
2016-05-02 09:23:06 +07:00
Kevin McCarthy
37c8cbca91 pep8 2016-05-01 14:44:13 -10:00
Aliaksandr Buhayeu
9daf301deb Fix for Serialization errors with JSON adapter
This patch aims to fix the issue#222,
where json data in request can not be serialized
because of TypeError in py3
2016-05-01 14:22:17 -10:00
Kevin McCarthy
528c9e7b1a Merge pull request #149 from kevin1024/pytest-httpbin
WIP: add pytest-httpbin
2016-05-01 14:19:54 -10:00
Kevin McCarthy
4e36997e1a Use pytest-httpbin
This will help the test flakiness and speed up test runs.
2016-05-01 13:50:04 -10:00
Kevin McCarthy
c571c932c9 Merge pull request #252 from jayvdb/travis-more-versions
Add Python 3.5 and PyPy 3 to Travis
2016-05-01 07:20:48 -10:00
John Vandenberg
d060a68ffd Add Python 3.5 and PyPy 3 to Travis 2016-05-01 07:54:59 +07:00
Kevin McCarthy
cfc483a08d Merge pull request #244 from jaysonsantos/master
Avoid concatenating bytes with strings
2016-04-26 08:59:07 -04:00
Jayson Reis
632af2e41a Fix/ignore some flake errors 2016-01-20 12:28:46 +01:00
Jayson Reis
7fdfce65ee Add test to make sure we can post chunked binary data 2016-01-20 12:17:28 +01:00
Jayson Reis
7cc513e1d2 Avoid concatenating bytes with strings 2016-01-19 14:19:03 +01:00
Ivan 'Goat' Malison
4f3c5c0a6e Merge pull request #243 from kevin1024/response_filtering
allow filtering by response
2016-01-11 17:12:44 -08:00
Ivan Malison
43b3411e6c Fix travis check for flakes 2016-01-11 17:00:02 -08:00
Ivan Malison
99d4150df8 allow filtering by response 2016-01-11 16:07:44 -08:00
Kevin McCarthy
8d5993eced Merge pull request #242 from koobs/patch-1
Exclude __pycache__ dirs & compiled files in sdist
2016-01-10 07:35:20 -10:00
Kubilay Kocak
8a1b7c6532 Exclude __pycache__ dirs & compiled files in sdist
The current vcrpy sdist on PyPI includes __pycache__ dirs and compiled files which causes tests to fail when running tests via setup.py test

import file mismatch:
imported module 'test_persist' has this __file__ attribute:
  /Users/imalison/Projects/vcrpy/tests/unit/test_persist.py
which is not the same as the test file we want to collect:
  /usr/home/user/repos/freebsd/ports/devel/py-vcrpy/work/vcrpy-1.7.4/tests/unit/test_persist.py
HINT: remove __pycache__ / .pyc files and/or use a unique basename for your test file modules

This change recursively excludes these directories and files from future source distributions
2016-01-11 02:27:41 +11:00
Ivan 'Goat' Malison
3459d95d4f Merge pull request #234 from tobiowo/decode-compressed-response
Decode compressed response option
2015-12-31 10:18:34 -08:00
Ivan 'Goat' Malison
ebaae9bed7 Merge pull request #240 from abhinav/old-tornado
Fix crashing with Tornado 3
2015-12-31 10:17:01 -08:00
Abhinav Gupta
d780bc04dd Fix Tornado support behavior for Tornado 3.
Resolves #235.
2015-12-19 18:18:20 -08:00
Kevin McCarthy
31c358c035 Merge pull request #238 from jayvdb/tox-travis
Use tox-travis
2015-12-12 10:51:07 -10:00
John Vandenberg
573c6eee0b Use tox-travis
dc9cd42 introduced Travis checking of flakes using tox.
However tox.ini only defined 'flakes-py34', so Travis
was only invoking flake8 on Python 3.4, and invoking
py.test on other Python versions.

As tox factors only work properly on the default testenv,
use a generic tox environment 'flakes', and use tox-travis
to select the correct basepython.
2015-12-13 04:22:51 +11:00
Kevin McCarthy
70c92d05d9 Merge pull request #229 from jayvdb/flakes
Fix pyflakes and pep8 errors
2015-12-05 14:06:53 -10:00
Olutobi Owoputi
5d866dd77c support python 3.4 2015-12-02 14:43:06 -08:00
Olutobi Owoputi
2d08358b5c tests / docs for decode_compressed_response 2015-12-02 12:26:23 -08:00
Olutobi Owoputi
64397d7ecc add decode_compressed_response option and filter 2015-12-02 12:25:36 -08:00
John Vandenberg
dc9cd4229b Fix pyflakes and pep8 errors
Use extra asserts to use previously unused variables in tests,
such as `cass` and `response`.

Fix only pyflakes errors in docs/conf.py
2015-11-26 08:25:06 +11:00
Kevin McCarthy
6ae1b00207 Merge pull request #228 from jayvdb/travis-tox
Use tox for Travis-CI
2015-11-25 09:23:28 -05:00
John Vandenberg
54bb9aa27a Use tox for Travis-CI 2015-11-25 15:51:25 +11:00
Aron Griffis
312ed2c234 Merge pull request #223 from jayvdb/jessie-fix
Fallback to importing from urllib3
2015-11-24 15:14:09 -05:00
Kevin McCarthy
20915a79c1 Merge pull request #226 from Bjwebb/patch-1
Fix typo
2015-11-14 07:22:33 -06:00
Ben Webb
495afdddc8 Fix typo 2015-11-14 07:41:38 +00:00
Aron Griffis
dee580f971 Merge pull request #196 from agriffis/remove-replace
Enable header replacement rather than removal
2015-11-07 19:01:00 -05:00
Aron Griffis
6919c06b8c Add documentation for new features of filter_headers, filter_query_parameters and filter_post_data_parameters 2015-11-07 18:50:32 -05:00
Aron Griffis
77de8dc47e Update VCR params to use new filters. 2015-11-07 17:05:25 -05:00
Aron Griffis
cb40a45eba Add replace_post_data_parameters() 2015-11-07 17:05:25 -05:00
Aron Griffis
678586904b Add replace_query_parameters() 2015-11-07 17:05:25 -05:00
Aron Griffis
ddbf0464f4 Add replace_headers() 2015-11-07 17:05:25 -05:00
Aron Griffis
e14b94789b Add note and link for vcrpy-unittest. 2015-11-07 17:04:56 -05:00
Aron Griffis
e6dba270ec Blindly add modules to be documented 2015-11-07 16:58:10 -05:00
Aron Griffis
615cf8661a Add PyPI badge. 2015-11-07 16:47:15 -05:00
Aron Griffis
ce6656c4d5 Rename waffle badge from 'ready' to 'waffle' which seems clearer. 2015-11-07 16:47:06 -05:00
Aron Griffis
8d083ba578 Split README.rst into appropriate docs sections 2015-11-07 16:38:02 -05:00
Aron Griffis
f0f5334c40 Use sphinx_rtd_theme building docs locally 2015-11-07 16:37:16 -05:00
Aron Griffis
8de2312ccc Switch to default RtD theme 2015-11-07 15:26:22 -05:00
Aron Griffis
c3f5ae84b1 Bump docs version to match project version 1.7.4 2015-11-07 15:23:14 -05:00
Ivan Malison
f6b8e4f8e7 Lint cleanup 2015-10-25 20:30:33 -07:00
Ivan Malison
2ac3fa9abe v1.7.4 2015-10-18 16:48:27 -07:00
John Vandenberg
dd8b39b29e Fallback to importing from urllib3
requests.packages.urllib3 may be literally urllib3
instead of vendored urllib3.
2015-10-15 14:56:30 +11:00
Ivan 'Goat' Malison
1324feae99 Merge pull request #217 from bcen/fix-decoration-no-return
Fix use_cassette when used as decorator does not return the wrapped function's return value
2015-10-02 00:11:20 -07:00
Bocai Cen
7990c549d1 fix decoration when in _handle_function does not return the wrapped function return value 2015-10-01 23:17:15 -04:00
Ivan 'Goat' Malison
327797c4ff Merge pull request #206 from tyewang/propogate_attribute_changes_to_real_connection_on_stubs
Attributes set on VCRConnection now also get set on the real_connection
2015-09-20 00:56:54 -07:00
Tye Wang
ac510097e0 Add TODO and elaborate on comment 2015-09-18 12:19:17 -04:00
Ivan Malison
00d973a0f5 sphinx skeleton 2015-09-13 11:27:08 -07:00
Tye Wang
79ff59feae Attributes set on VCRConnection now also get set on the real_connection 2015-09-03 14:35:28 -04:00
Ivan 'Goat' Malison
34252bc234 Merge pull request #192 from agriffis/insensitive-headers
Make request.headers always a CaseInsensitiveDict.
2015-08-28 14:47:45 -07:00
Ivan Malison
5f78657c52 test_boto cleanup 2015-08-28 13:03:09 -07:00
Aron Griffis
00b4e451fe Full image URL in README.rst
...so it shows properly on PyPI
2015-08-28 14:46:08 -04:00
Aron Griffis
44564ba39f Merge pull request #198 from gwillem/patch-1
Add instructions on using libyaml
2015-08-28 13:01:14 -04:00
Aron Griffis
7f02a7e999 Fix a bad merge in README.rst
This change seems to have gotten lost in #200
2015-08-28 12:57:05 -04:00
Ivan Malison
c28adea66d Fix typo in release notes 2015-08-28 09:20:54 -07:00
Aron Griffis
3f006cc261 Merge pull request #201 from agriffis/path-transformer-default
Default path_transformer=None. Fixes #199
2015-08-28 07:55:13 -04:00
Aron Griffis
0eda8ba482 Default path_transformer=None. Fixes #199 2015-08-28 07:25:03 -04:00
Ivan 'Goat' Malison
d620095c36 Merge pull request #200 from kevin1024/test_class_meta
Test class meta
2015-08-28 03:19:54 -07:00
Ivan Malison
c8180326ad Automatically decorate dynamically added methods with auto_decorate 2015-08-28 02:17:01 -07:00
Willem de Groot
d55d593d1c Changed bash to sh and fix syntax
Online Github editting is not very convenient after all :)
2015-08-28 11:14:01 +02:00
Willem de Groot
04f4a7fd2f Add bash code 2015-08-28 11:11:03 +02:00
Ivan Malison
6fd04f3675 Add test_case method to VCR
this method provides a class that can be inherited from to decorate all
test methods on the desired class with use_cassette using the relevant vcr
2015-08-28 01:44:39 -07:00
Ivan Malison
420f83b6b1 Move badges to top of README. 2015-08-28 01:44:39 -07:00
Aron Griffis
c6adcc83b3 Spello on use_cassette, thanks @Diaoul 2015-08-27 13:39:01 -04:00
Willem de Groot
dc61f5f520 Typoe 2015-08-27 11:20:11 +02:00
Willem de Groot
4450cb992f Add instructions on using libyaml
For a 10x speed increase!
2015-08-27 11:19:31 +02:00
Aron Griffis
083b1ec686 Move gitter chat with other badges 2015-08-26 17:34:45 -04:00
Ivan 'Goat' Malison
97c924d8dd Merge pull request #197 from gitter-badger/gitter-badge
Add a Gitter chat badge to README.rst
2015-08-26 11:49:26 -07:00
The Gitter Badger
65398131a4 Added Gitter badge 2015-08-26 17:29:20 +00:00
Aron Griffis
7312229aef Add HeadersDict, and mark add_header deprecated.
HeadersDict is a subclass of CaseInsensitiveDict with two new features:

  1. Preserve the case of the header key from the first time it was set.
     This means that later munging won't modify the key case. (You can
     force picking up the new case with `del` followed by setting.)

  2. If the value is a list or tuple, unpack it and store the first
     element. This is the same as how `Request.add_header()` used to work.

For backward compatibility this commit preserves `Request.add_header()` but
marks it deprecated.
2015-08-25 06:30:50 -04:00
Ivan 'Goat' Malison
b62265c0ad Merge pull request #194 from agriffis/travis-requests26
Add requests-2.6.0 to travis
2015-08-24 15:14:12 -07:00
Ivan 'Goat' Malison
d00c60a4ad Merge pull request #193 from agriffis/new-travis
Container-based travis for faster tests
2015-08-24 15:13:59 -07:00
Aron Griffis
4ddfb47c9c Add requests-2.6.0 to travis
This is tested already in tox, but seems to have been omitted accidentally
from the travis config.
2015-08-24 16:48:54 -04:00
Aron Griffis
f0b7c3f1e0 Container-based travis for faster tests 2015-08-24 16:28:29 -04:00
Aron Griffis
646d12df94 More compact expression with dict.get() 2015-08-24 16:19:34 -04:00
Aron Griffis
eda64bc3be Make request.headers always a CaseInsensitiveDict.
Previously request.headers was a normal dict (albeit with the
request.add_header interface) which meant that some code paths would do
case-sensitive matching, for example remove_post_data_parameters which
tests for 'Content-Type'. This change allows all code paths to get the same
case-insensitive treatment.

Additionally request.headers becomes a property to enforce upgrading it to
a CaseInsensitiveDict even if assigned.
2015-08-24 16:19:34 -04:00
Ivan Malison
efe6744eda v1.7.3 2015-08-23 18:10:01 -07:00
Ivan 'Goat' Malison
58f4b98f7f Merge pull request #191 from agriffis/trivial-fixes
Trivial cleanups and one bugfix
2015-08-23 13:33:34 -07:00
Aron Griffis
3305f0ca7d Repair a docstring 2015-08-23 16:33:25 -04:00
Aron Griffis
7f02d65dd9 Make hosts_to_ignore a set() earlier for clarity. 2015-08-23 16:00:52 -04:00
Aron Griffis
3e5553c56a Don't drop passed-in before_record_response
This is an actual bugfix. If before_record_response is passed into VCR as
an iterable then it won't be included in filter_functions. This commit
repairs the logic to separate the tests (just as it's already done for
before_record_request).

Also use .extend() rather than looping on .append()
2015-08-23 16:00:52 -04:00
Aron Griffis
a569dd4dc8 Raise KeyError with message instead of print, just like in get_matchers below 2015-08-23 12:37:01 -04:00
Aron Griffis
eb1cdad03a self._before_record_response can never be falsy in Cassette (just like self._before_record_request above it) 2015-08-23 12:37:01 -04:00
Aron Griffis
08bb3bd187 Remove an extra space 2015-08-23 12:37:01 -04:00
Ivan Malison
ae5580c8f9 style changes in test_vcr.py 2015-08-22 18:59:59 -07:00
Ivan Malison
f342f92f03 additional_matchers test 2015-08-22 18:58:12 -07:00
Ivan Malison
be3bf39161 Style changes in vcr/config.py 2015-08-22 18:48:04 -07:00
Ivan Malison
29d37e410a Add additional_matchers to use_cassette
Closes #188.
2015-08-22 18:06:13 -07:00
Ivan Malison
8b7e6c0ab8 v1.7.2 2015-08-18 17:00:45 -07:00
Ivan Malison
bd7c6ed03f Update comment about reentrance on cassette.py 2015-08-18 16:17:41 -07:00
Ivan 'Goat' Malison
1e414826e7 Merge pull request #187 from abhinav/master
Set request_time on Tornadoo HTTPResponses
2015-08-18 16:10:42 -07:00
Abhinav Gupta
1e1c093b3c Set request_time on Tornadoo HTTPResponses 2015-08-18 15:53:35 -07:00
Ivan 'Goat' Malison
bb8f563135 Merge pull request #186 from ByteInternet/capture-effective-url
Capture effective url in Tornado
2015-08-18 01:55:24 -07:00
Maarten van Schaik
ca3200d96e Add test for urllib2 2015-08-14 12:42:17 +02:00
Maarten van Schaik
04b5978adc Add effective url test for httplib2 2015-08-14 12:37:34 +02:00
Maarten van Schaik
01f1f9fdc1 Verify effective_url is ok 2015-08-14 12:29:50 +02:00
Maarten van Schaik
a82e8628c2 Requests actually stores redirected request 2015-08-14 12:28:41 +02:00
Maarten van Schaik
7d68f0577a Capture effective URL in tornado 2015-08-14 12:08:57 +02:00
Ivan Malison
d0aa5fddb7 1.7.1 2015-08-12 12:21:29 -07:00
Kevin McCarthy
e54aeadc68 Merge pull request #184 from abhinav/master
For Tornado AsyncHTTPClient, replace the methods instead of the class.
2015-08-12 09:16:22 -10:00
Abhinav Gupta
c4a33d1cff For Tornado AsyncHTTPClient, replace the methods instead of the class.
This makes it so patching works even if the user has a reference to, or an
instance of the original unpatched AsyncHTTPClient class.

Fixes #183.
2015-08-12 10:51:08 -07:00
Ivan Malison
8b59d73f25 v1.7.0 2015-08-02 13:36:57 -07:00
Ivan 'Goat' Malison
eb394b90d9 Merge pull request #181 from coagulant/fix-readme-custom-response
Fix example for custom response filtering in docs
2015-08-01 06:06:34 -07:00
Ilya Baryshev
14931dd47a Fix example for custom response filtering in docs 2015-08-01 11:10:51 +03:00
Ivan Malison
89cdda86d1 fix generator test. 2015-07-30 14:22:16 -07:00
Ivan 'Goat' Malison
ad48d71897 Merge pull request #180 from abhinav/master
Fix exception catching in coroutines.
2015-07-30 14:21:44 -07:00
Abhinav Gupta
946ce17a97 Fix exception catching in coroutines. 2015-07-30 14:13:58 -07:00
Ivan Malison
4d438dac75 Fix tornado python3 tests. 2015-07-30 04:19:17 -07:00
Ivan Malison
a234ad6b12 Fix all the tests in python 3 2015-07-30 03:39:04 -07:00
Ivan Malison
1d000ac652 Fix all the failing tests 2015-07-30 02:08:42 -07:00
Ivan Malison
21c176ee1e Make cassette active for duration of coroutine/generator
Closes #177.
2015-07-30 01:47:29 -07:00
Ivan 'Goat' Malison
4fb5bef8e1 Merge pull request #179 from graingert/support-ancient-PyPA-tools
Support distribute, Fixes #178
2015-07-29 23:22:16 -07:00
Thomas Grainger
9717596e2c Support distribute, Fixes #178 2015-07-29 22:19:45 +01:00
Ivan 'Goat' Malison
1660cc3a9f Merge pull request #176 from charlax/patch-1
Make setup example pep8 compliant
2015-07-27 10:37:42 -07:00
Charles-Axel Dein
4beb023204 Make setup example pep8 compliant
Pretty minor doc change.
2015-07-27 18:22:51 +02:00
Ivan 'Goat' Malison
72eb5345d6 Merge pull request #175 from gward/issue163-v2
Fix for #163, take 2.
2015-07-26 02:40:27 -07:00
Greg Ward
fe7d193d1a Add several more test cases for issue #163. 2015-07-16 14:49:48 -04:00
Greg Ward
09b7ccf561 Ensure that request bodies are always bytes, not text (fixes #163).
It shouldn't matter whether the request body comes from a file or a
string, or whether it is passed to the Request constructor or assigned
later. It should always be stored internally as bytes.
2015-07-16 14:36:26 -04:00
Kevin McCarthy
a4a80b431b Merge pull request #173 from graingert/patch-2
Fix before_record_reponse doc
2015-07-16 07:28:25 -10:00
Thomas Grainger
025a3b422d Fix before_record_reponse doc 2015-07-16 15:13:19 +01:00
Kevin McCarthy
bb05b2fcf7 Merge pull request #172 from abhinav/patch-1
Add Tornado to list of supported libraries
2015-07-15 10:31:51 -10:00
Abhinav Gupta
f77ef81877 Add Tornado to list of supported libraries 2015-07-15 12:43:36 -07:00
Ivan Malison
80ece7750f v1.6.1 2015-07-15 00:25:56 -07:00
Ivan Malison
8a86d75dc5 Merge remote-tracking branch 'upstream/master' into improved_body_matcher 2015-07-15 00:10:37 -07:00
Ivan Malison
33a4fb98c6 Update unit tests for body matcher. Simplified logic. 2015-07-15 00:01:31 -07:00
Diaoul
a046697567 Add a read_body helper function 2015-07-15 01:16:10 +02:00
Diaoul
c0286dfd97 Add body matcher unit tests 2015-07-11 23:22:42 +02:00
Diaoul
cc9af1d5fb Use CaseInsensitiveDict in body matcher 2015-07-11 23:18:45 +02:00
Ivan 'Goat' Malison
5f8407a8a1 Merge pull request #170 from graingert/manual-conditional-requirements-for-old-pip
Support conditional requirements in old versions of pip
2015-07-07 16:09:46 -07:00
Thomas Grainger
c789c82c1d Support conditional requirements in old versions of pip 2015-07-07 11:28:49 +01:00
Ivan 'Goat' Malison
16b5b77bcd Merge pull request #168 from graingert/patch-1
Fix RST parse errors generated by pandoc
2015-07-05 12:43:43 -07:00
Thomas Grainger
0a093786ed Fix RST parse errors generated by pandoc 2015-07-05 12:14:01 +01:00
Diaoul
3986caf182 Use Content-Type based approach for body matcher
When converting objects to body, dicts and sets order can change
resulting in a different but same body. This fixes the issue by
comparing the enclosed data in the body rather than the body itself
while still allowing raw body matching with the raw_body matcher.
2015-07-04 19:21:14 +02:00
Ivan 'Goat' Malison
cc6c26646c Merge pull request #165 from abhinav/master
[Tornado] Fix unsupported features exception not being raised.
2015-07-03 16:04:49 -07:00
Abhinav Gupta
3846a4ccef [Tornado] Fix unsupported features exception not being raised.
Add tests for that exception being raisd correctly and for
CannotOverwriteCassetteException.
2015-07-03 12:34:57 -07:00
Ivan Malison
aae4ae255b README spacing fix. 2015-07-03 10:26:52 -07:00
Ivan Malison
92303a911a v1.6.0 2015-07-03 10:17:44 -07:00
Ivan 'Goat' Malison
57e0e6c753 Merge pull request #164 from abhinav/tornado
Tornado support
2015-07-03 10:01:20 -07:00
Abhinav Gupta
c37d607b97 Don't install pycurl if pypy is being used.
Pycurl doesn't yet support pypy.
2015-07-02 17:14:00 -07:00
Abhinav Gupta
7922fec9fe Tornado support 2015-07-02 14:33:34 -07:00
Ivan 'Goat' Malison
7d175b0f91 Merge pull request #162 from graingert/conditional-requirements
use conditional requirements for backport libraries closes #147
2015-07-02 09:25:10 -07:00
Thomas Grainger
41949f7dc6 use conditional requirements for backport libraries Fixes #147 2015-07-02 09:34:27 +01:00
Ivan 'Goat' Malison
d14888ccd8 Merge pull request #161 from graingert/packaging-fixes
packaging fixes
2015-06-30 12:02:38 -07:00
Thomas Grainger
a9ede54064 packaging fixes 2015-06-30 12:19:05 +01:00
Ivan 'Goat' Malison
ce7ceb0a1e Merge pull request #159 from MrJohz/master
Fix issue #158
2015-06-25 01:04:12 -04:00
Jonathan
e742d32a8a Update test to ensure that filter is correctly applied 2015-06-24 16:27:45 +01:00
Jonathan
ccc1ccaa0e Allow filtering post params in requests 2015-06-24 16:23:00 +01:00
Ivan Malison
731a33a79a test for xmlrpclib to make sure #140 is actually fixed. 2015-06-09 01:13:58 -07:00
Antoine Bertin
6cbc0fb279 Fix httplib endheaders if a message_body exists
Fixes #140
2015-06-09 00:59:43 -07:00
Ivan Malison
789f118c98 whitespace change. 2015-06-09 00:54:54 -07:00
Ivan Malison
4f07cb5257 Fix tox.ini requests26: requests==2.6.0 2015-06-09 00:53:28 -07:00
Ivan Malison
ad6f635ac2 Version 1.5.2. 2015-05-15 16:54:40 -07:00
Ivan 'Goat' Malison
142b840eee Merge pull request #155 from gazpachoking/explicit_paths
Crash when cassette path contains cassette_library_dir
2015-05-15 16:51:03 -07:00
Chase Sterling
32c687522d Fix bug when specifying cassette path containing cassette_library_dir 2015-05-15 19:07:18 -04:00
Chase Sterling
5fc33c7e70 Add tests for explicitly specifying a path when library dir is defined 2015-05-15 19:06:05 -04:00
Ivan Malison
0f81f023c8 Fix readme, version 1.5.1 2015-05-14 14:46:14 -07:00
Ivan Malison
e324a9677d version 1.5.0 2015-05-14 14:05:50 -07:00
Ivan Malison
28640beb7d README updates. 2015-05-14 14:03:49 -07:00
Ivan 'Goat' Malison
c338d5d32c Merge pull request #154 from marco-santamaria/master
Filter parameters from 'application/json' content-type POST requests
2015-05-14 14:03:14 -07:00
marco.santamaria
59aa351ca8 Added support for json post data in filter_post_data_parameters. 2015-05-14 14:13:14 +02:00
Ivan Malison
2323b9da5f Automatically generate cassette names from function names. Add
`path_transformer` and `func_path_generator`. Closes #151.
2015-05-10 03:22:43 -07:00
Ivan Malison
0bbbc694b0 Make CassetteContextDecorator decorator produce reentrant functions.
Closes #150.
2015-05-09 23:14:00 -07:00
Kevin McCarthy
d293020617 Merge pull request #153 from addgene/mw/specify-six-version
Fix version of `six` dependency.
2015-05-07 20:34:38 -10:00
Morgan Wahl
daac863f0b Fixed version of six dependency.
`from six.moves.http_client import HTTPConnection` fails before version 1.5.0 of six. (on Python 2.7, at least.)
2015-05-07 15:35:26 -04:00
Kevin McCarthy
5cfb005b48 bump version 2015-05-05 22:22:37 -10:00
Kevin McCarthy
4ade547779 python3 uses capital headers sometimes, let's do a case-agnostic header removal 2015-05-05 21:59:48 -10:00
Kevin McCarthy
dc8eedf555 we dont actually need lxml for tests 2015-05-05 21:59:31 -10:00
Kevin McCarthy
5b9b6cd8b5 dont try to load chunked responses from cassettes, we already unchunked everything 2015-05-05 21:32:32 -10:00
Kevin McCarthy
856c38479a add failing test for requests2.7/gzip issue 2015-05-05 20:49:12 -10:00
Kevin McCarthy
52496cd091 really add requests 2.7 to tox 2015-05-05 19:50:20 -10:00
Kevin McCarthy
bc26ce877a add requests 2.7 to tox and travis 2015-05-05 19:44:19 -10:00
Kevin McCarthy
8db0d245a5 bump version 2015-04-11 11:21:31 -10:00
Kevin McCarthy
47544b08fe Merge pull request #148 from ralphbean/master
Ship extra bits with the pypi tarball.
2015-04-11 11:18:57 -10:00
Ralph Bean
4e560fc8db Ship extra bits with the pypi tarball.
This change should make new tarballs uploaded to pypi include various
nice pieces:

- The README
- The LICENSE
- The tests

The text of the license actually specifies that the full text must be
distributed with all copies of the software.  So, you need it to be in
compliance with the MIT license.

The README is just nice to have, and the tests are particularly nice for
my use case.  I am packaging vcrpy for inclusion in the Fedora linux
distribution and:

- We like to use the tarball from pypi because it is the same source
  distribution that everyone else is using.
- We like to run the tests before we build the rpm in our build system
  to make sure nothing crazy is going on.

Of course, we can use the tarball for the source and then do a second
step to clone the source and get the tests.  But, this is more work than
we like if we can just get the tests added to the tarball.  Other
distributions (like Debian) like this too.
2015-04-11 14:53:48 -04:00
Ivan Malison
8bb3c6beee v1.4.0 2015-04-02 12:20:16 -07:00
Ivan Malison
df3ad5f35c remove compat.py in favor of backport_collections. 2015-04-02 10:32:34 -07:00
Ivan Malison
e8a6a7a49f add backport_collections, tweaks to setup.py. 2015-04-02 10:23:22 -07:00
Ivan Malison
881138cb8d inject_cassette fallout. 2015-04-01 17:46:35 -07:00
Ivan Malison
639dba6f7a Write test for #145 that checks behavior of with_current_defaults. 2015-04-01 17:30:05 -07:00
Ivan Malison
b9bdc6401d inject_cassette kwarg. 2015-04-01 17:30:05 -07:00
Ivan Malison
3ca5529d26 Touch ups. 2015-04-01 15:38:59 -07:00
Sam Stavinoha
e3f2bc8369 fix with_current_defaults causing TypeError
The from_args() method in cassette.py was
throwing a TypeError when calling

    use_cassette(..., with_current_defaults=True)
    ...
    TypeError: from_args() takes exactly 3 arguments (4 given)

The path was then being passed to use() twice.
2015-03-31 21:08:26 -05:00
Edward Stone
fc4e985ee9 fallback to compat OrderedDict if collections.OrderedDict unavailable 2015-03-31 13:12:13 -07:00
Edward Stone
9038bc9066 fix docs for post data filter 2015-03-31 13:12:13 -07:00
Edward Stone
0def349420 Add ability to filter post data parameters 2015-03-31 13:12:13 -07:00
Ivan Malison
0dd7b05990 Get rid of all the constructor parameters that were removed in 0871c3b87c 2015-03-31 13:03:11 -07:00
Ivan Malison
630088599f Update copyright. 2015-03-31 13:02:59 -07:00
Ivan Malison
870ab276c4 Possible fix for #140. 2015-03-25 13:01:55 -07:00
Ivan 'Goat' Malison
779f3b0474 Merge pull request #141 from IvanMalison/post_files_through_requests
Add support for posting files through requests. closes #121
2015-03-24 16:43:13 -07:00
Ivan Malison
b948ed4857 Fix python3 support for requests file uploads. 2015-03-24 15:41:14 -07:00
Ivan Malison
c43e618635 Add mention of urllib3 support in readme. 2015-03-24 14:24:47 -07:00
Ivan Malison
5bd40a447a Add setter to body on vcr's request. 2015-03-24 14:11:16 -07:00
Ivan Malison
4b4be7f661 Don't use 2.7+ style ',' separated with. 2015-03-24 14:11:16 -07:00
Ivan Malison
6602a449b1 Add support for posting files through requests. closes #121. Possibly #134. 2015-03-24 14:11:16 -07:00
Ivan Malison
7cd7264034 fix tox/urllib3 stuff. 2015-03-24 14:10:14 -07:00
Ivan Malison
e9c690b9e7 Version 1.3.0. 2015-03-23 18:10:26 -07:00
Ivan Malison
bba5df2fbb clarifying comment in patch.py. 2015-03-23 18:00:23 -07:00
Ivan Malison
39c3b15e02 unused imports. 2015-03-23 17:56:46 -07:00
Ivan Malison
c87e6d6f6a Clarifying comments in patch.py. 2015-03-23 17:55:49 -07:00
Ivan Malison
5ab77e22db Use suggested emacs style coding statements (see https://www.python.org/dev/peps/pep-0263/). 2015-03-23 17:55:49 -07:00
Ivan 'Goat' Malison
ec6f27bbad Merge pull request #138 from aisch/patch-and-test-urllib3
update urllib3 patch/stub to be same as used for requests and add tests
2015-03-23 17:47:42 -07:00
aisch
8930c97ff7 rm unused imports 2015-03-23 13:56:48 -07:00
aisch
e6b43a0374 rename urllib3 patch method and rm unused imports from tests 2015-03-23 13:43:30 -07:00
aisch
63ec95be06 update urllib3 patch/stub to be same as used for requests and add tests 2015-03-23 12:12:49 -07:00
Kevin McCarthy
84c45b2742 Merge pull request #136 from abhinav/https-port-fix
Fix default port for HTTPS
2015-02-24 09:12:49 -10:00
Abhinav Gupta
87a25e9ab0 Fix httplib2 integration test. 2015-02-24 00:10:08 -08:00
Abhinav Gupta
2473bdb77a Fix default port for HTTPS. 2015-02-23 23:37:04 -08:00
Ivan 'Goat' Malison
32831d4151 Merge pull request #135 from RomuloOliveira/patch-1
Fix missing quotes on Custom Response Filtering
2015-01-28 12:02:57 -08:00
Rômulo Oliveira
4991d6f1c8 Fix missing quotes on Custom Response Filtering
Missing quotes are bad
2015-01-28 11:34:47 -02:00
Ivan Malison
14ef1e87f7 Add custom_patches section to README.md 2015-01-08 14:02:41 -08:00
Ivan 'Goat' Malison
fb14739cc1 Merge pull request #133 from IvanMalison/custom_patches
Custom patches
2015-01-08 11:08:55 -08:00
Ivan Malison
a7c7e4e279 Bump version to 1.2.0 2015-01-08 10:56:39 -08:00
Ivan Malison
c0a22df7ed Add ability to add custom patches to vcr and cassettes. 2015-01-08 10:54:27 -08:00
Ivan Malison
83aed99058 Bump vesrsion to 1.1.4, add to release notes. 2014-12-26 05:26:24 -05:00
Ivan Malison
e1f65bcbdc Add force reset around calls to actual connection from stubs, to ensure
compatibility with version of httplib/urlib2 in python 2.7.9. Closes #130.
2014-12-26 05:10:20 -05:00
Kevin McCarthy
5301149bd8 Merge pull request #128 from gazpachoking/patch-1
Update changelog to note requests 2.5 support
2014-12-09 08:55:52 -10:00
Chase Sterling
0297fcdde7 Update changelog to note requests 2.5 support 2014-12-09 13:26:46 -05:00
Kevin McCarthy
9480954c33 update release notes 2014-12-08 17:10:35 -10:00
Kevin McCarthy
8432ad32f1 Merge pull request #127 from gazpachoking/1.1.3
Version bump to v1.1.3
2014-12-08 17:09:07 -10:00
Chase Sterling
fabef3d988 Version bump to v1.1.3 2014-12-08 21:43:01 -05:00
Ivan 'Goat' Malison
da45f46b2d Merge pull request #125 from gazpachoking/pool_is_none
Fix crash with requests 2.5 where connectionpool was None
2014-12-08 13:20:36 -08:00
Ivan 'Goat' Malison
562a0ebadc Merge pull request #126 from gazpachoking/116
Play back requests requests on windows. fix #116
2014-12-08 12:29:34 -08:00
Chase Sterling
ef8ba6d51b Add requests 2.5 to testing list in .travis.yml and tox.ini 2014-12-08 14:40:55 -05:00
Chase Sterling
f6aa6eac84 Play back requests requests on windows. fix #116 2014-12-08 14:28:48 -05:00
Chase Sterling
821e148752 Fix crash with requests 2.5 where connectionpool was None 2014-12-07 13:49:23 -05:00
Ivan Malison
7306205b8a Improve test_new_episodes_record_mode_two_times test. 2014-11-21 17:15:15 -08:00
Nithin Reddy
2a128893cc Adds a test to ensure that the cassette created with "new_episodes" has different expected behavior when opened with "once". 2014-11-21 09:47:28 -08:00
Nithin Reddy
5162d183e5 Fixes #123. When attempting to replay the same request twice using record_mode="new_episodes", vcr.py raises UnhandledHTTPRequestError. 2014-11-20 19:07:21 -08:00
Ivan Malison
9d52c3ed42 Remove warning message caused by lack of is_verified property on HTTPSConnection stub. 2014-11-13 16:32:38 -08:00
Ivan 'Goat' Malison
0e37759175 Merge pull request #118 from rtaboada/fix-response-stub-headers-field
Create headers field in VCRHTTPResponse. Fixes #117.
2014-11-03 04:07:12 -08:00
Ivan 'Goat' Malison
78c6258ba3 Merge pull request #119 from telaviv/make_boto_tests_pass_again
test_boto_stubs passes again.
2014-10-31 00:14:45 -07:00
Shawn Krisman
b047336690 test_boto_stubs passes again. 2014-10-30 16:08:17 -07:00
Rodrigo Taboada
c955a5ea88 String in request body should be bytes. 2014-10-24 18:30:32 -02:00
Rodrigo Taboada
5423d99f5a Tests for VCRHTTPResponse headers field. 2014-10-24 17:40:51 -02:00
Rodrigo Taboada
a71c15f398 Create headers field in VCRHTTPResponse. Fixes #117. 2014-10-24 16:37:12 -02:00
Ivan Malison
6e049ba7a1 version bump to v1.1.2 2014-10-08 12:11:53 -07:00
Ivan Malison
916e7839e5 Actually use pytest.raises in test. 2014-10-07 13:45:09 -07:00
Ivan Malison
99692a92d2 Handle unicode error in json serialize properly. 2014-10-07 13:21:47 -07:00
Ivan Malison
a9a68ba44b Random tweaks. 2014-10-05 18:37:01 -07:00
Ivan Malison
e9f35db405 Remove .travis.yml changes. 2014-10-05 16:42:46 -07:00
Ivan Malison
7193407a07 Remove ipdb because it causes python below 2.6 to blow up. 2014-10-03 01:40:02 -07:00
Ivan Malison
c3427ae3a2 Fix pip install of tox in travis. 2014-10-02 15:48:29 -07:00
Ivan Malison
3a46a6f210 travis through tox. 2014-10-02 15:26:22 -07:00
Ivan Malison
163181844b Refactor tox.ini using new 1.8 features. 2014-10-02 14:57:53 -07:00
Ivan Malison
2c6f072d11 better logging when matches aren't working. 2014-09-25 04:49:00 -07:00
Ivan Malison
361ed82a10 Bump version to 1.1.1 2014-09-22 19:22:52 -07:00
Ivan Malison
0871c3b87c Remove instance variables for filter_headers, filter_query_params, ignore_localhost and ignore_hosts. These still exist on the VCR object, but they are automatically translated into a filter function when passed to the cassette. 2014-09-22 17:57:22 -07:00
Ivan 'Goat' Malison
d484dee50f Merge pull request #110 from IvanMalison/use_cassette_decorator_pytest_compatibility
Fix use_cassette decorator in python 2 by using wrapt.decorator
2014-09-22 17:02:23 -07:00
Ivan Malison
b046ee4bb1 Fix use_cassette decorator in python 2 by using wrapt.decorator. Add wrapt as dependency. 2014-09-22 16:40:09 -07:00
Ivan 'Goat' Malison
3dea853482 Merge pull request #108 from IvanMalison/fix_CassetteContextDecorator_nesting_issues
Fix cassette context decorator nesting issues
2014-09-21 05:18:41 -07:00
Ivan Malison
113c95f971 Bump setup.py version to v1.1.0. minor tweaks to readme. 2014-09-21 05:14:30 -07:00
Ivan Malison
a2c947dc48 Fix last bit of of #109. 2014-09-21 05:06:28 -07:00
Ivan Malison
757ad9c836 Revert "Remove ConnectionRemover class that tried to get rid of vcr connections in ConnectionPools."
This reverts commit dc249b0965.

Conflicts:
	vcr/patch.py
2014-09-20 11:59:25 -07:00
Ivan Malison
18e5898ec4 Return a tuple from the _request function on CassettePatcherBuilder even if import fails. Make _recursively_apply_get_cassette_subclass actually work with dictionaries. 2014-09-20 11:28:59 -07:00
Ivan Malison
83211a1887 Make changes from b1cdd50e9b compatible with requests1.x; Update Readme.md with description of before_record_response 2014-09-20 11:05:25 -07:00
Ivan Malison
dc249b0965 Remove ConnectionRemover class that tried to get rid of vcr connections in ConnectionPools. 2014-09-19 19:02:59 -07:00
Ivan Malison
121ed79172 Mark bad test xfail. 2014-09-19 17:10:19 -07:00
Ivan Malison
b1cdd50e9b Fix some of the issues from #109 2014-09-19 17:06:53 -07:00
Ivan Malison
1018867838 Revert "Fixed issue in test_nested_context_managers_with_session_created_before_first_nesting. by using a single class and patching cassette on that class. Not a great solution :\"
This reverts commit 2bf23b2cdf.
2014-09-19 14:32:21 -07:00
Ivan Malison
b6e96020c1 Use {[testenv]deps}, instead of repeating testing requirements. Write another failing test for #109 2014-09-19 14:31:49 -07:00
Ivan Malison
8947f0fc5c Add failing test for session still being attached to cassette after context is gone. 2014-09-18 17:03:13 -07:00
Ivan Malison
2bf23b2cdf Fixed issue in test_nested_context_managers_with_session_created_before_first_nesting. by using a single class and patching cassette on that class. Not a great solution :\ 2014-09-18 17:01:48 -07:00
Ivan Malison
58fcb2b453 Add test that fails because of the fact that a new class is used for each cassette context instead of replacing the cassette on the existing mock connection. 2014-09-18 16:17:48 -07:00
Ivan Malison
0c19acd74f Use contextdecorator from contextlib2. add logging for entering context. 2014-09-18 15:25:42 -07:00
Ivan Malison
4868a63876 Refactor build_patchers into class. Fix issue with patching non existent attribute with hasattr. 2014-09-18 14:42:56 -07:00
Ivan Malison
e1e08c7a2c hasattr check for requests 2.0 use cassette added type for httplib2 dictionary patch. 2014-09-18 08:02:50 -07:00
Ivan Malison
5edc58f10c Check for old style class when building subclass. 2014-09-18 07:10:52 -07:00
Ivan Malison
2193008150 Python version agnostic way of getting the next item in the generator. 2014-09-18 05:59:03 -07:00
Ivan Malison
958aac3af3 Use mock for patching http connection objects. 2014-09-18 05:32:55 -07:00
Ivan Malison
9a564586a4 Failing tests for nested context decoration. 2014-09-18 03:46:39 -07:00
Ivan Malison
643a4c91ee Change use_cassette to pass a function to CassetteContextDecorator so that changes to the default settings on the vcr properly propogate. 2014-09-18 02:52:44 -07:00
Ivan Malison
472cc3bffe use_cassette -> CassetteContextDecorator 2014-09-17 23:22:43 -07:00
Ivan Malison
8db46002a3 Fix failure in test_local_host resulting from attempting to add tuple to list. 2014-09-17 21:48:28 -07:00
Ivan Malison
a08c90c5d6 Revert "Add global toggle to use_cassette."
This reverts commit 366e2b75bb.

Conflicts:
	tests/unit/test_cassettes.py
2014-09-17 21:42:25 -07:00
Ivan Malison
8e01426056 Change default paramters to VCR from lists to tuples. 2014-09-17 19:29:02 -07:00
Ivan Malison
9a4f5f23a4 Add before_record_response to Cassette and VCR. 2014-09-17 04:10:05 -07:00
Ivan Malison
366e2b75bb Add global toggle to use_cassette. 2014-09-17 01:28:54 -07:00
Ivan Malison
0cfe63ef6e Bump version number for new use_cassette_decorator. 2014-09-16 23:53:50 -07:00
Ivan Malison
cb05f4163c Add use_cassette class so functinos that are decorated with use_cassette can be called multiple times. 2014-09-16 23:45:05 -07:00
Kevin McCarthy
20057a6815 Merge pull request #101 from IvanMalison/minor_cleanup
Clean up __init__.py .
2014-09-15 19:10:40 -10:00
Ivan Malison
0d313502b8 Clean up __init__.py . 2014-09-15 21:10:24 -07:00
Kevin McCarthy
d9c2b4b25d Merge pull request #100 from IvanMalison/immutable_keyword_defaults
Make defaults of Cassette constructor immutable. Clean up whitespace. Re...
2014-09-12 14:01:55 -10:00
Ivan Malison
640681138a Make defaults of Cassette constructor immutable. Clean up whitespace. Remove unused import. 2014-09-12 02:16:32 -07:00
Kevin McCarthy
a02bbbab2b Merge pull request #97 from matt-thomson/multiple-headers
Fix serialization/deserialization of multiple headers with the same value
2014-09-07 10:06:26 -10:00
Matt Thomson
f719f90e63 Fix multiple header behaviour.
Join multiple header values together, rather than losing duplicates with a
dict.
2014-09-06 17:15:15 +01:00
Matt Thomson
3c410b5f9d Don't write header values multiple times.
On Python 3, response.msg.keys() contains the same value multiple times if
there are multiple headers with the same value.  Work around this by
converting to a set before iterating over it.
2014-09-06 16:57:12 +01:00
Matt Thomson
7a5795a547 Add test to demonstrate Python 3 multiple headers bug. 2014-09-06 16:32:29 +01:00
Kevin McCarthy
1bd3fbd2c6 bump version 2014-09-01 15:50:25 -10:00
Kevin McCarthy
cd715f37c6 Fix requests stub. Closes #94 2014-09-01 15:48:57 -10:00
Kevin McCarthy
9a1147196a getheader() in stubs should be case-insensitive 2014-08-01 16:28:21 -10:00
Kevin McCarthy
a23c5d8508 Merge pull request #87 from hartsock/master
Python 3: print_function
2014-07-24 18:10:24 -10:00
Shawn Hartsock
868a974900 Python 3: print_function
Use print function if you must print, this lets us use the
library in python 3 environments.

partial: https://github.com/kevin1024/vcrpy/issues/86
2014-07-24 16:33:47 -04:00
Kevin McCarthy
c56de472cd Remove extra colon in README
Closes #82
2014-07-07 16:02:29 -10:00
Kevin McCarthy
c6590f2caf bump version 2014-05-17 13:09:10 -10:00
Kevin McCarthy
70abc5058c requests 2.3 compat 2014-05-17 12:58:31 -10:00
119 changed files with 10271 additions and 2804 deletions

7
.codecov.yml Normal file
View File

@@ -0,0 +1,7 @@
coverage:
status:
project:
default:
target: 75
# Allow 0% coverage regression
threshold: 0

14
.editorconfig Normal file
View File

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

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-24.04
steps:
- name: Checkout
uses: actions/checkout@v5
- name: Codespell
uses: codespell-project/actions-codespell@v2

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

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

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

@@ -0,0 +1,61 @@
name: Test
on:
push:
branches:
- master
pull_request:
schedule:
- cron: "0 16 * * 5" # Every Friday 4pm
workflow_dispatch:
jobs:
build:
runs-on: ubuntu-24.04
strategy:
fail-fast: false
matrix:
python-version:
- "3.10"
- "3.11"
- "3.12"
- "3.13"
- "pypy-3.11"
steps:
- uses: actions/checkout@v5
- name: Install uv
uses: astral-sh/setup-uv@v7
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v6
with:
python-version: ${{ matrix.python-version }}
allow-prereleases: true
- name: Install project dependencies
run: |
uv pip install --system --upgrade pip setuptools
uv pip install --system codecov '.[tests]'
uv pip check
- name: Allow creation of user namespaces (e.g. to the unshare command)
run: |
# .. so that we don't get error:
# unshare: write failed /proc/self/uid_map: Operation not permitted
# Idea from https://github.com/YoYoGames/GameMaker-Bugs/issues/6015#issuecomment-2135552784 .
sudo sysctl kernel.apparmor_restrict_unprivileged_userns=0
- name: Run online tests
run: ./runtests.sh --cov=./vcr --cov-branch --cov-report=xml --cov-append -m online
- name: Run offline tests with no access to the Internet
run: |
# We're using unshare to take Internet access
# away so that we'll notice whenever some new test
# is missing @pytest.mark.online decoration in the future
unshare --map-root-user --net -- \
sh -c 'ip link set lo up; ./runtests.sh --cov=./vcr --cov-branch --cov-report=xml --cov-append -m "not online"'
- name: Run coverage
run: codecov

View File

@@ -0,0 +1,62 @@
# Copyright (c) 2023 Sebastian Pipping <sebastian@pipping.org>
# Licensed under the MIT license
name: Detect outdated pre-commit hooks
on:
schedule:
- cron: '0 16 * * 5' # Every Friday 4pm
# NOTE: This will drop all permissions from GITHUB_TOKEN except metadata read,
# and then (re)add the ones listed below:
permissions:
contents: write
pull-requests: write
jobs:
pre_commit_detect_outdated:
name: Detect outdated pre-commit hooks
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v5
- name: Set up Python 3.12
uses: actions/setup-python@v6
with:
python-version: 3.12
- name: Install pre-commit
run: |-
pip install \
--disable-pip-version-check \
--no-warn-script-location \
--user \
pre-commit
echo "PATH=${HOME}/.local/bin:${PATH}" >> "${GITHUB_ENV}"
- name: Check for outdated hooks
run: |-
pre-commit autoupdate
git diff -- .pre-commit-config.yaml
- name: Create pull request from changes (if any)
id: create-pull-request
uses: peter-evans/create-pull-request@v7
with:
author: 'pre-commit <pre-commit@tools.invalid>'
base: master
body: |-
For your consideration.
:warning: Please **CLOSE AND RE-OPEN** this pull request so that [further workflow runs get triggered](https://github.com/peter-evans/create-pull-request/blob/main/docs/concepts-guidelines.md#triggering-further-workflow-runs) for this pull request.
branch: precommit-autoupdate
commit-message: "pre-commit: Autoupdate"
delete-branch: true
draft: true
labels: enhancement
title: "pre-commit: Autoupdate"
- name: Log pull request URL
if: "${{ steps.create-pull-request.outputs.pull-request-url }}"
run: |
echo "Pull request URL is: ${{ steps.create-pull-request.outputs.pull-request-url }}"

20
.github/workflows/pre-commit.yml vendored Normal file
View File

@@ -0,0 +1,20 @@
# Copyright (c) 2023 Sebastian Pipping <sebastian@pipping.org>
# Licensed under the MIT license
name: Run pre-commit
on:
- pull_request
- push
- workflow_dispatch
jobs:
pre-commit:
name: Run pre-commit
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v5
- uses: actions/setup-python@v6
with:
python-version: 3.12
- uses: pre-commit/action@v3.0.1

9
.gitignore vendored
View File

@@ -1,8 +1,17 @@
*.pyc
.tox
.cache
.pytest_cache/
build/
dist/
*.egg/
.coverage
coverage.xml
htmlcov/
*.egg-info/
pytestdebug.log
pip-wheel-metadata/
.python-version
fixtures/
/docs/_build

17
.pre-commit-config.yaml Normal file
View File

@@ -0,0 +1,17 @@
# Copyright (c) 2023 Sebastian Pipping <sebastian@pipping.org>
# Licensed under the MIT license
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.14.6
hooks:
- id: ruff
args: ["--output-format=full"]
- id: ruff-format
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v6.0.0
hooks:
- id: check-merge-conflict
- id: end-of-file-fixer
- id: trailing-whitespace

24
.readthedocs.yaml Normal file
View File

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

View File

@@ -1,29 +0,0 @@
language: python
before_install: openssl version
env:
global:
- secure: AifoKzwhjV94cmcQZrdQmqRu/9rkZZvWpwBv1daeAQpLOKFPGsOm3D+x2cSw9+iCfkgDZDfqQVv1kCaFVxTll8v8jTq5SJdqEY0NmGWbj/UkNtShh609oRDsuzLxAEwtVKYjf/h8K2BRea+bl1tGkwZ2vtmYS6dxNlAijjWOfds=
- secure: LBSEg/gMj4u4Hrpo3zs6Y/1mTpd2RtcN49mZIFgTdbJ9IhpiNPqcEt647Lz94F9Eses2x2WbNuKqZKZZReY7QLbEzU1m0nN5jlaKrjcG5NR5clNABfFFyhgc0jBikyS4abAG8jc2efeaTrFuQwdoF4sE8YiVrkiVj2X5Xoi6sBk=
matrix:
- WITH_LIB="requests2.x"
- WITH_LIB="requests1.x"
- WITH_LIB="httplib2"
- WITH_LIB="boto"
matrix:
allow_failures:
- env: WITH_LIB="boto"
exclude:
- env: WITH_LIB="boto"
python: 3.3
python:
- 2.6
- 2.7
- 3.3
- pypy
install:
- pip install PyYAML pytest pytest-localserver --use-mirrors
- if [ $WITH_LIB = "requests1.x" ] ; then pip install requests==1.2.3; fi
- if [ $WITH_LIB = "requests2.x" ] ; then pip install requests; fi
- if [ $WITH_LIB = "httplib2" ] ; then pip install httplib2; fi
- if [ $WITH_LIB = "boto" ] ; then pip install boto; fi
script: python setup.py test

View File

@@ -1,4 +1,4 @@
Copyright (c) 2012-2014 Kevin McCarthy
Copyright (c) 2012-2015 Kevin McCarthy
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

5
MANIFEST.in Normal file
View File

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

509
README.md
View File

@@ -1,509 +0,0 @@
# VCR.py
![vcr.py](https://raw.github.com/kevin1024/vcrpy/master/vcr.png)
This is a Python version of [Ruby's VCR library](https://github.com/myronmarston/vcr).
[![Build Status](https://secure.travis-ci.org/kevin1024/vcrpy.png?branch=master)](http://travis-ci.org/kevin1024/vcrpy)
[![Stories in Ready](https://badge.waffle.io/kevin1024/vcrpy.png?label=ready&title=Ready)](https://waffle.io/kevin1024/vcrpy)
## What it does
Simplify and speed up testing HTTP by recording all HTTP interactions and
saving them to "cassette" files, which are yaml files containing the contents
of your requests and responses. Then when you run your tests again, they all
just hit the text files instead of the internet. This speeds up your tests and
lets you work offline.
If the server you are testing against ever changes its API, all you need to do
is delete your existing cassette files, and run your tests again. All of the
mocked responses will be updated with the new API.
## Compatibility Notes
VCR.py supports Python 2.6 and 2.7, 3.3, 3.4, and [pypy](http://pypy.org).
The following http libraries are supported:
* urllib2
* http.client (python3)
* requests (both 1.x and 2.x versions)
* httplib2
* boto
## Usage
```python
import vcr
import urllib2
with vcr.use_cassette('fixtures/vcr_cassettes/synopsis.yaml'):
response = urllib2.urlopen('http://www.iana.org/domains/reserved').read()
assert 'Example domains' in response
```
Run this test once, and VCR.py will record the HTTP request to
`fixtures/vcr_cassettes/synopsis.yml`. Run it again, and VCR.py will replay the
response from iana.org when the http request is made. This test is now fast (no
real HTTP requests are made anymore), deterministic (the test will continue to
pass, even if you are offline, or iana.org goes down for maintenance) and
accurate (the response will contain the same headers and body you get from a
real request).
You can also use VCR.py as a decorator. The same request above would look like
this:
```python
@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
```
All of the parameters and configuration works the same for the decorator
version.
## Configuration
If you don't like VCR's defaults, you can set options by instantiating a `VCR`
class and setting the options on it.
```python
import vcr
my_vcr = vcr.VCR(
serializer = 'json',
cassette_library_dir = 'fixtures/cassettes',
record_mode = 'once',
match_on = ['uri', 'method'],
)
with my_vcr.use_cassette('test.json'):
# your http code here
```
Otherwise, you can override options each time you use a cassette.
```python
with vcr.use_cassette('test.yml', serializer='json', record_mode='once'):
# your http code here
```
Note: Per-cassette overrides take precedence over the global config.
## Request matching
Request matching is configurable and allows you to change which requests VCR
considers identical. The default behavior is
`['method', 'scheme', 'host', 'port', 'path', 'query']` which means that
requests with both the same URL and method (ie POST or GET) are considered
identical.
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.)
* host (the hostname of the server receiving the request)
* port (the port of the server receiving the request)
* path (the path of the request)
* query (the query string of the request)
* body (the entire request body)
* headers (the headers of the request)
Backwards compatible matchers:
* url (the `uri` alias)
If these options don't work for you, you can also register your own request
matcher. This is described in the Advanced section of this README.
## Record Modes
VCR supports 4 record modes (with the same behavior as Ruby's VCR):
### once
* Replay previously recorded interactions.
* Record new interactions if there is no cassette file.
* Cause an error to be raised for new requests if there is a cassette file.
It is similar to the new_episodes record mode, but will prevent new, unexpected
requests from being made (i.e. because the request URI changed).
once is the default record mode, used when you do not set one.
### new_episodes
* Record new interactions.
* Replay previously recorded interactions. It is similar to the once record
mode, but will always record new interactions, even if you have an existing
recorded one that is similar, but not identical.
This was the default behavior in versions < 0.3.0
### none
* Replay previously recorded interactions.
* Cause an error to be raised for any new requests. This is useful when your
code makes potentially dangerous HTTP requests. The none record mode
guarantees that no new HTTP requests will be made.
### all
* Record new interactions.
* Never replay previously recorded interactions. This can be temporarily used
to force VCR to re-record a cassette (i.e. to ensure the responses are not
out of date) or can be used when you simply want to log all HTTP requests.
## Advanced Features
If you want, VCR.py can return information about the cassette it is using to
record your requests and responses. This will let you record your requests and
responses and make assertions on them, to make sure that your code under test
is generating the expected requests and responses. This feature is not present
in Ruby's VCR, but I think it is a nice addition. Here's an example:
```python
import vcr
import urllib2
with vcr.use_cassette('fixtures/vcr_cassettes/synopsis.yaml') as cass:
response = urllib2.urlopen('http://www.zombo.com/').read()
# cass should have 1 request inside it
assert len(cass) == 1
# the request uri should have been http://www.zombo.com/
assert cass.requests[0].uri == 'http://www.zombo.com/'
```
The `Cassette` object exposes the following properties which I consider part of
the API. The fields are as follows:
* `requests`: A list of vcr.Request objects containing the requests made while
this cassette was being used, ordered by the order that the request was made.
* `responses`: A list of the responses made.
* `play_count`: The number of times this cassette has had a response played
back
* `all_played`: A boolean indicates whether all the responses have been
played back
* `responses_of(request)`: Access the responses that match a given request
The `Request` object has the following properties:
* `uri`: The full uri of the request. Example: "https://google.com/?q=vcrpy"
* `scheme`: The scheme used to make the request (http or https)
* `host`: The host of the request, for example "www.google.com"
* `port`: The port the request was made on
* `path`: The path of the request. For example "/" or "/home.html"
* `query`: The parsed query string of the request. Sorted list of name, value pairs.
* `method` : The method used to make the request, for example "GET" or "POST"
* `body`: The body of the request, usually empty except for POST / PUT / etc
Backwards compatible properties:
* `url`: The `uri` alias
* `protocol`: The `scheme` alias
## Register your own serializer
Don't like JSON or YAML? That's OK, VCR.py can serialize to any format you
would like. Create your own module or class instance with 2 methods:
* `def deserialize(cassette_string)`
* `def serialize(cassette_dict)`
Finally, register your class with VCR to use your new serializer.
```python
import vcr
BogoSerializer(object):
"""
Must implement serialize() and deserialize() methods
"""
pass
my_vcr = vcr.VCR()
my_vcr.register_serializer('bogo', BogoSerializer())
with my_vcr.use_cassette('test.bogo', serializer='bogo'):
# your http here
# After you register, you can set the default serializer to your new serializer
my_vcr.serializer = 'bogo'
with my_vcr.use_cassette('test.bogo'):
# your http here
```
## Register your own request matcher
Create your own method with the following signature
```python
def my_matcher(r1, r2):
```
Your method receives the two requests and must return `True` if they match,
`False` if they don't.
Finally, register your method with VCR to use your new request matcher.
```python
import vcr
def jurassic_matcher(r1, r2):
return r1.uri == r2.uri and 'JURASSIC PARK' in r1.body
my_vcr = vcr.VCR()
my_vcr.register_matcher('jurassic', jurassic_matcher)
with my_vcr.use_cassette('test.yml', match_on=['jurassic']):
# your http here
# After you register, you can set the default match_on to use your new matcher
my_vcr.match_on = ['jurassic']
with my_vcr.use_cassette('test.yml'):
# your http here
```
## Filter sensitive data from the request
If you are checking your cassettes into source control, and are using some form
of authentication in your tests, you can filter out that information so it won't
appear in your cassette files. There are a few ways to do this:
### Filter information from HTTP Headers
Use the `filter_headers` configuration option with a list of headers to filter.
```python
with my_vcr.use_cassette('test.yml', filter_headers=['authorization']):
# sensitive HTTP request goes here
```
### Filter information from HTTP querystring
Use the `filter_query_parameters` configuration option with a list of query
parameters to filter.
```python
with my_vcr.use_cassette('test.yml', filter_query_parameters=['api_key']):
requests.get('http://api.com/getdata?api_key=secretstring')
```
### Custom request filtering
If neither of these covers your use case, you can register a callback that will
manipulate the HTTP request before adding it to the cassette. Use the
`before_record` configuration option to so this. Here is an
example that will never record requests to the /login endpoint.
```python
def before_record_cb(request):
if request.path != '/login':
return request
my_vcr = vcr.VCR(
before_record = before_record_cb,
)
with my_vcr.use_cassette('test.yml'):
# your http code here
```
## Ignore requests
If you would like to completely ignore certain requests, you can do it in a
few ways:
* Set the `ignore_localhost` option equal to True. This will not record any
requests sent to (or responses from) localhost, 127.0.0.1, or 0.0.0.0.
* Set the `ignore_hosts` configuration option to a list of hosts to ignore
* Add a `before_record` callback that returns None for requests you want to
ignore
Requests that are ignored by VCR will not be saved in a cassette, nor played
back from a cassette. VCR will completely ignore those requests as if it
didn't notice them at all, and they will continue to hit the server as if VCR
were not there.
## Installation
VCR.py is a package on PyPI, so you can `pip install vcrpy` (first you may need
to `brew install libyaml` [[Homebrew](http://mxcl.github.com/homebrew/)])
## Ruby VCR compatibility
I'm not trying to match the format of the Ruby VCR YAML files. Cassettes
generated by Ruby's VCR are not compatible with VCR.py.
## 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:
`tox -e py27requests -- -v -k "'test_status_code or test_gzip'"`
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 that has `requests`
installed.
Also, in order for the boto tests to run, you will need an AWS key. Refer to
the [boto
documentation](http://boto.readthedocs.org/en/latest/getting_started.html) for
how to set this up. I have marked the boto tests as optional in Travis so you
don't have to worry about them failing if you submit a pull request.
## Logging
VCR.py has a few log messages you can turn on to help you figure out if HTTP
requests are hitting a real server or not. You can turn them on like this:
```python
import vcr
import requests
import logging
logging.basicConfig() # you need to initialize logging, otherwise you will not see anything from vcrpy
vcr_log = logging.getLogger("vcr")
vcr_log.setLevel(logging.INFO)
with vcr.use_cassette('headers.yml'):
requests.get('http://httpbin.org/headers')
```
The first time you run this, you will see:
```
INFO:vcr.stubs:<Request (GET) http://httpbin.org/headers> not in cassette, sending to real server
```
The second time, you will see:
```
INFO:vcr.stubs:Playing response for <Request (GET) http://httpbin.org/headers> from cassette
```
If you set the loglevel to DEBUG, you will also get information about which
matchers didn't match. This can help you with debugging custom matchers.
## Upgrade
### New Cassette Format
The cassette format has changed in _VCR.py 1.x_, the _VCR.py 0.x_ cassettes
cannot be used with _VCR.py 1.x_. The easiest way to upgrade 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
```
The PATH can be either a path to the directory with cassettes or the path to a
single cassette.
*Note*: Back up your cassettes files before migration.
The migration *should* only modify cassettes using the old 0.x format.
## New serializer / deserializer API
If you made a custom serializer, you will need to update it to match the new
API in version 1.0.x
* Serializers now take dicts and return strings.
* Deserializers take strings and return dicts (instead of requests, responses
pair)
## Changelog
* 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
# License
This library uses the MIT license. See [LICENSE.txt](LICENSE.txt) for more details

72
README.rst Normal file
View File

@@ -0,0 +1,72 @@
###########
VCR.py 📼
###########
|PyPI| |Python versions| |Build Status| |CodeCov| |Gitter|
----
.. image:: https://vcrpy.readthedocs.io/en/latest/_images/vcr.svg
:alt: vcr.py logo
This is a Python version of `Ruby's VCR
library <https://github.com/vcr/vcr>`__.
Source code
https://github.com/kevin1024/vcrpy
Documentation
https://vcrpy.readthedocs.io/
Rationale
---------
VCR.py simplifies and speeds up tests that make HTTP requests. The
first time you run code that is inside a VCR.py context manager or
decorated function, VCR.py records all HTTP interactions that take
place through the libraries it supports and serializes and writes them
to a flat file (in yaml format by default). This flat file is called a
cassette. When the relevant piece of code is executed again, VCR.py
will read the serialized requests and responses from the
aforementioned cassette file, and intercept any HTTP requests that it
recognizes from the original test run and return the responses that
corresponded to those requests. This means that the requests will not
actually result in HTTP traffic, which confers several benefits
including:
- The ability to work offline
- Completely deterministic tests
- Increased test execution speed
If the server you are testing against ever changes its API, all you need
to do is delete your existing cassette files, and run your tests again.
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
.. |PyPI| image:: https://img.shields.io/pypi/v/vcrpy.svg
: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://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
.. |CodeCov| image:: https://codecov.io/gh/kevin1024/vcrpy/branch/master/graph/badge.svg
:target: https://codecov.io/gh/kevin1024/vcrpy
:alt: Code Coverage Status

192
docs/Makefile Normal file
View File

@@ -0,0 +1,192 @@
# Makefile for Sphinx documentation
#
# You can set these variables from the command line.
SPHINXOPTS =
SPHINXBUILD = sphinx-build
PAPER =
BUILDDIR = _build
# User-friendly check for sphinx-build
ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1)
$(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/)
endif
# Internal variables.
PAPEROPT_a4 = -D latex_paper_size=a4
PAPEROPT_letter = -D latex_paper_size=letter
ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
# the i18n builder cannot share the environment and doctrees with the others
I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest coverage gettext
help:
@echo "Please use \`make <target>' where <target> is one of"
@echo " html to make standalone HTML files"
@echo " dirhtml to make HTML files named index.html in directories"
@echo " singlehtml to make a single large HTML file"
@echo " pickle to make pickle files"
@echo " json to make JSON files"
@echo " htmlhelp to make HTML files and a HTML help project"
@echo " qthelp to make HTML files and a qthelp project"
@echo " applehelp to make an Apple Help Book"
@echo " devhelp to make HTML files and a Devhelp project"
@echo " epub to make an epub"
@echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
@echo " latexpdf to make LaTeX files and run them through pdflatex"
@echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx"
@echo " text to make text files"
@echo " man to make manual pages"
@echo " texinfo to make Texinfo files"
@echo " info to make Texinfo files and run them through makeinfo"
@echo " gettext to make PO message catalogs"
@echo " changes to make an overview of all changed/added/deprecated items"
@echo " xml to make Docutils-native XML files"
@echo " pseudoxml to make pseudoxml-XML files for display purposes"
@echo " linkcheck to check all external links for integrity"
@echo " doctest to run all doctests embedded in the documentation (if enabled)"
@echo " coverage to run coverage check of the documentation (if enabled)"
clean:
rm -rf $(BUILDDIR)/*
html:
$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
dirhtml:
$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
singlehtml:
$(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
@echo
@echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
pickle:
$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
@echo
@echo "Build finished; now you can process the pickle files."
json:
$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
@echo
@echo "Build finished; now you can process the JSON files."
htmlhelp:
$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
@echo
@echo "Build finished; now you can run HTML Help Workshop with the" \
".hhp project file in $(BUILDDIR)/htmlhelp."
qthelp:
$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
@echo
@echo "Build finished; now you can run "qcollectiongenerator" with the" \
".qhcp project file in $(BUILDDIR)/qthelp, like this:"
@echo "# qcollectiongenerator $(BUILDDIR)/qthelp/vcrpy.qhcp"
@echo "To view the help file:"
@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/vcrpy.qhc"
applehelp:
$(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp
@echo
@echo "Build finished. The help book is in $(BUILDDIR)/applehelp."
@echo "N.B. You won't be able to view it unless you put it in" \
"~/Library/Documentation/Help or install it in your application" \
"bundle."
devhelp:
$(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
@echo
@echo "Build finished."
@echo "To view the help file:"
@echo "# mkdir -p $$HOME/.local/share/devhelp/vcrpy"
@echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/vcrpy"
@echo "# devhelp"
epub:
$(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
@echo
@echo "Build finished. The epub file is in $(BUILDDIR)/epub."
latex:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo
@echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
@echo "Run \`make' in that directory to run these through (pdf)latex" \
"(use \`make latexpdf' here to do that automatically)."
latexpdf:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo "Running LaTeX files through pdflatex..."
$(MAKE) -C $(BUILDDIR)/latex all-pdf
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
latexpdfja:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo "Running LaTeX files through platex and dvipdfmx..."
$(MAKE) -C $(BUILDDIR)/latex all-pdf-ja
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
text:
$(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
@echo
@echo "Build finished. The text files are in $(BUILDDIR)/text."
man:
$(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
@echo
@echo "Build finished. The manual pages are in $(BUILDDIR)/man."
texinfo:
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
@echo
@echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
@echo "Run \`make' in that directory to run these through makeinfo" \
"(use \`make info' here to do that automatically)."
info:
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
@echo "Running Texinfo files through makeinfo..."
make -C $(BUILDDIR)/texinfo info
@echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
gettext:
$(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
@echo
@echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
changes:
$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
@echo
@echo "The overview file is in $(BUILDDIR)/changes."
linkcheck:
$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
@echo
@echo "Link check complete; look for any errors in the above output " \
"or in $(BUILDDIR)/linkcheck/output.txt."
doctest:
$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
@echo "Testing of doctests in the sources finished, look at the " \
"results in $(BUILDDIR)/doctest/output.txt."
coverage:
$(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage
@echo "Testing of coverage in the sources finished, look at the " \
"results in $(BUILDDIR)/coverage/python.txt."
xml:
$(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml
@echo
@echo "Build finished. The XML files are in $(BUILDDIR)/xml."
pseudoxml:
$(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml
@echo
@echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml."

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

442
docs/advanced.rst Normal file
View File

@@ -0,0 +1,442 @@
Advanced Features
=================
If you want, VCR.py can return information about the cassette it is
using to record your requests and responses. This will let you record
your requests and responses and make assertions on them, to make sure
that your code under test is generating the expected requests and
responses. This feature is not present in Ruby's VCR, but I think it is
a nice addition. Here's an example:
.. code:: python
import vcr
import urllib2
with vcr.use_cassette('fixtures/vcr_cassettes/synopsis.yaml') as cass:
response = urllib2.urlopen('http://www.zombo.com/').read()
# cass should have 1 request inside it
assert len(cass) == 1
# the request uri should have been http://www.zombo.com/
assert cass.requests[0].uri == 'http://www.zombo.com/'
The ``Cassette`` object exposes the following properties which I
consider part of the API. The fields are as follows:
- ``requests``: A list of vcr.Request objects corresponding to the http
requests that were made during the recording of the cassette. The
requests appear in the order that they were originally processed.
- ``responses``: A list of the responses made.
- ``play_count``: The number of times this cassette has played back a
response.
- ``all_played``: A boolean indicating whether all the responses have
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:
- ``uri``: The full uri of the request. Example:
"https://google.com/?q=vcrpy"
- ``scheme``: The scheme used to make the request (http or https)
- ``host``: The host of the request, for example "www.google.com"
- ``port``: The port the request was made on
- ``path``: The path of the request. For example "/" or "/home.html"
- ``query``: The parsed query string of the request. Sorted list of
name, value pairs.
- ``method`` : The method used to make the request, for example "GET"
or "POST"
- ``body``: The body of the request, usually empty except for POST /
PUT / etc
Backwards compatible properties:
- ``url``: The ``uri`` alias
- ``protocol``: The ``scheme`` alias
Register your own serializer
----------------------------
Don't like JSON or YAML? That's OK, VCR.py can serialize to any format
you would like. Create your own module or class instance with 2 methods:
- ``def deserialize(cassette_string)``
- ``def serialize(cassette_dict)``
Finally, register your class with VCR to use your new serializer.
.. code:: python
import vcr
class BogoSerializer:
"""
Must implement serialize() and deserialize() methods
"""
pass
my_vcr = vcr.VCR()
my_vcr.register_serializer('bogo', BogoSerializer())
with my_vcr.use_cassette('test.bogo', serializer='bogo'):
# your http here
# After you register, you can set the default serializer to your new serializer
my_vcr.serializer = 'bogo'
with my_vcr.use_cassette('test.bogo'):
# your http here
Register your own request matcher
---------------------------------
Create your own method with the following signature
.. code:: python
def my_matcher(r1, r2):
Your method receives the two requests and can either:
- Use an ``assert`` statement: return None if they match and raise ``AssertionError`` if not.
- Return a boolean: ``True`` if they match, ``False`` if not.
Note: in order to have good feedback when a matcher fails, we recommend using an ``assert`` statement with a clear error message.
Finally, register your method with VCR to use your new request matcher.
.. code:: python
import vcr
def jurassic_matcher(r1, r2):
assert r1.uri == r2.uri and 'JURASSIC PARK' in r1.body, \
'required string (JURASSIC PARK) not found in request body'
my_vcr = vcr.VCR()
my_vcr.register_matcher('jurassic', jurassic_matcher)
with my_vcr.use_cassette('test.yml', match_on=['jurassic']):
# your http here
# After you register, you can set the default match_on to use your new matcher
my_vcr.match_on = ['jurassic']
with my_vcr.use_cassette('test.yml'):
# your http here
Register your own cassette persister
------------------------------------
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
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...
.. code:: python
import vcr
my_vcr = vcr.VCR()
class CustomerPersister:
# implement Persister methods...
my_vcr.register_persister(CustomPersister)
Filter sensitive data from the request
--------------------------------------
If you are checking your cassettes into source control, and are using
some form of authentication in your tests, you can filter out that
information so it won't appear in your cassette files. There are a few
ways to do this:
Filter information from HTTP Headers
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Use the ``filter_headers`` configuration option with a list of headers
to filter.
.. code:: python
with my_vcr.use_cassette('test.yml', filter_headers=['authorization']):
# sensitive HTTP request goes here
Filter information from HTTP querystring
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Use the ``filter_query_parameters`` configuration option with a list of
query parameters to filter.
.. code:: python
with my_vcr.use_cassette('test.yml', filter_query_parameters=['api_key']):
requests.get('http://api.com/getdata?api_key=secretstring')
Filter information from HTTP post data
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Use the ``filter_post_data_parameters`` configuration option with a list
of post data parameters to filter.
.. code:: python
with my_vcr.use_cassette('test.yml', filter_post_data_parameters=['api_key']):
requests.post('http://api.com/postdata', data={'api_key': 'secretstring'})
Advanced use of filter_headers, filter_query_parameters and filter_post_data_parameters
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
In all of the above cases, it's also possible to pass a list of ``(key, value)``
tuples where the value can be any of the following:
* A new value to replace the original value.
* ``None`` to remove the key/value pair. (Same as passing a simple key string.)
* A callable that returns a new value or ``None``.
So these two calls are the same:
.. code:: python
# original (still works)
vcr = VCR(filter_headers=['authorization'])
# new
vcr = VCR(filter_headers=[('authorization', None)])
Here are two examples of the new functionality:
.. code:: python
# replace with a static value (most common)
vcr = VCR(filter_headers=[('authorization', 'XXXXXX')])
# replace with a callable, for example when testing
# lots of different kinds of authorization.
def replace_auth(key, value, request):
auth_type = value.split(' ', 1)[0]
return '{} {}'.format(auth_type, 'XXXXXX')
Custom Request filtering
~~~~~~~~~~~~~~~~~~~~~~~~
If none of these covers your request filtering needs, you can register a
callback with the ``before_record_request`` configuration option to
manipulate the HTTP request before adding it to the cassette, or return
``None`` to ignore it entirely. Here is an example that will never record
requests to the ``'/login'`` path:
.. code:: python
def before_record_cb(request):
if request.path == '/login':
return None
return request
my_vcr = vcr.VCR(
before_record_request=before_record_cb,
)
with my_vcr.use_cassette('test.yml'):
# your http code here
You can also mutate the request using this callback. For example, you
could remove all query parameters from any requests to the ``'/login'``
path.
.. code:: python
def scrub_login_request(request):
if request.path == '/login':
request.uri, _ = urllib.splitquery(request.uri)
return request
my_vcr = vcr.VCR(
before_record_request=scrub_login_request,
)
with my_vcr.use_cassette('test.yml'):
# your http code here
Custom Response Filtering
~~~~~~~~~~~~~~~~~~~~~~~~~
You can also do response filtering with the
``before_record_response`` configuration option. Its usage is
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 response body:
.. code:: python
def scrub_string(string, replacement=''):
def before_record_response(response):
response['body']['string'] = response['body']['string'].replace(string, replacement)
return response
return before_record_response
my_vcr = vcr.VCR(
before_record_response=scrub_string(settings.USERNAME, 'username'),
)
with my_vcr.use_cassette('test.yml'):
# your http code here
Decode compressed response
---------------------------
When the ``decode_compressed_response`` keyword argument of a ``VCR`` object
is set to True, VCR will decompress "gzip" and "deflate" response bodies
before recording. This ensures that these interactions become readable and
editable after being serialized.
.. note::
Decompression is done before any other specified `Custom Response Filtering`_.
This option should be avoided if the actual decompression of response bodies
is part of the functionality of the library or app being tested.
Ignore requests
---------------
If you would like to completely ignore certain requests, you can do it
in a few ways:
- Set the ``ignore_localhost`` option equal to True. This will not
record any requests sent to (or responses from) localhost, 127.0.0.1,
or 0.0.0.0.
- Set the ``ignore_hosts`` configuration option to a list of hosts to
ignore
- Add a ``before_record_request`` or ``before_record_response`` callback
that returns ``None`` for requests you want to ignore (see above).
Requests that are ignored by VCR will not be saved in a cassette, nor
played back from a cassette. VCR will completely ignore those requests
as if it didn't notice them at all, and they will continue to hit the
server as if VCR were not there.
Custom Patches
--------------
If you use a custom ``HTTPConnection`` class, or otherwise make http
requests in a way that requires additional patching, you can use the
``custom_patches`` keyword argument of the ``VCR`` and ``Cassette``
objects to patch those objects whenever a cassette's context is entered.
To patch a custom version of ``HTTPConnection`` you can do something
like this:
::
import where_the_custom_https_connection_lives
from vcr.stubs import VCRHTTPSConnection
my_vcr = config.VCR(custom_patches=((where_the_custom_https_connection_lives, 'CustomHTTPSConnection', VCRHTTPSConnection),))
@my_vcr.use_cassette(...)
Automatic Cassette Naming
-------------------------
VCR.py now allows the omission of the path argument to the use\_cassette
function. Both of the following are now legal/should work
.. code:: python
@my_vcr.use_cassette
def my_test_function():
...
.. code:: python
@my_vcr.use_cassette()
def my_test_function():
...
In both cases, VCR.py will use a path that is generated from the
provided test function's name. If no ``cassette_library_dir`` has been
set, the cassette will be in a file with the name of the test function
in directory of the file in which the test function is declared. If a
``cassette_library_dir`` has been set, the cassette will appear in that
directory in a file with the name of the decorated function.
It is possible to control the path produced by the automatic naming
machinery by customizing the ``path_transformer`` and
``func_path_generator`` vcr variables. To add an extension to all
cassette names, use ``VCR.ensure_suffix`` as follows:
.. code:: python
my_vcr = VCR(path_transformer=VCR.ensure_suffix('.yaml'))
@my_vcr.use_cassette
def my_test_function():
Rewind Cassette
---------------
VCR.py allows to rewind a cassette in order to replay it inside the same function/test.
.. code:: python
with vcr.use_cassette('fixtures/vcr_cassettes/synopsis.yaml') as cass:
response = urllib2.urlopen('http://www.zombo.com/').read()
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')
Drop unused requests
--------------------
Even if any HTTP request is changed or removed from tests, previously recorded
interactions remain in the cassette file. If set the ``drop_unused_requests``
option to ``True``, VCR will not save old HTTP interactions if they are not used.
.. code:: python
my_vcr = VCR(drop_unused_requests=True)
with my_vcr.use_cassette('fixtures/vcr_cassettes/synopsis.yaml'):
... # your HTTP interactions here

51
docs/api.rst Normal file
View File

@@ -0,0 +1,51 @@
API
===
:mod:`~vcr.config`
------------------
.. automodule:: vcr.config
:members:
:special-members: __init__
:mod:`~vcr.cassette`
--------------------
.. automodule:: vcr.cassette
:members:
:special-members: __init__
:mod:`~vcr.matchers`
--------------------
.. automodule:: vcr.matchers
:members:
:special-members: __init__
:mod:`~vcr.filters`
-------------------
.. automodule:: vcr.filters
:members:
:special-members: __init__
:mod:`~vcr.request`
-------------------
.. automodule:: vcr.request
:members:
:special-members: __init__
:mod:`~vcr.serialize`
---------------------
.. automodule:: vcr.serialize
:members:
:special-members: __init__
:mod:`~vcr.patch`
-----------------
.. automodule:: vcr.patch
:members:
:special-members: __init__

325
docs/changelog.rst Normal file
View File

@@ -0,0 +1,325 @@
Changelog
---------
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>`_
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.
- 8.0.0
- BREAKING: Drop support for Python 3.9 (major version bump) - thanks @jairhenrique
- BREAKING: Drop support for urllib3 < 2 - fixes CVE warnings from urllib3 1.x (#926, #880) - thanks @jairhenrique
- New feature: ``drop_unused_requests`` option to remove unused interactions from cassettes (#763) - thanks @danielnsilva
- Rewrite httpx support to patch httpcore instead of httpx (#943) - thanks @seowalex
- Fixes ``httpx.ResponseNotRead`` exceptions (#832, #834)
- Fixes ``KeyError: 'follow_redirects'`` (#945)
- Adds support for custom httpx transports
- Fix HTTPS proxy handling - proxy address no longer ends up in cassette URIs (#809, #914) - thanks @alga
- Fix ``iscoroutinefunction`` deprecation warning on Python 3.14 - thanks @kloczek
- Only log message if response is appended - thanks @talfus-laddus
- Optimize urllib.parse calls - thanks @Martin-Brunthaler
- Fix CI for Ubuntu 24.04 - thanks @hartwork
- Various CI improvements: migrate to uv, update GitHub Actions - thanks @jairhenrique
- Various linting and test improvements - thanks @jairhenrique and @hartwork
- 7.0.0
- Drop support for python 3.8 (major version bump) - thanks @jairhenrique
- Various linting and test fixes - thanks @jairhenrique
- Bugfix for urllib2>=2.3.0 - missing version_string (#888)
- Bugfix for asyncio.run - thanks @alekeik1
- 6.0.2
- Ensure body is consumed only once (#846) - thanks @sathieu
- Permit urllib3 2.x for non-PyPy Python >=3.10
- Fix typos in test commands - thanks @chuckwondo
- Several test and workflow improvements - thanks @hartwork and @graingert
- 6.0.1
- Bugfix with to Tornado cassette generator (thanks @graingert)
- 6.0.0
- BREAKING: Fix issue with httpx support (thanks @parkerhancock) in #784. NOTE: You may have to recreate some of your cassettes produced in previous releases due to the binary format being saved incorrectly in previous releases
- BREAKING: Drop support for `boto` (vcrpy still supports boto3, but is dropping the deprecated `boto` support in this release. (thanks @jairhenrique)
- Fix compatibility issue with Python 3.12 (thanks @hartwork)
- Drop simplejson (fixes some compatibility issues) (thanks @jairhenrique)
- Run CI on Python 3.12 and PyPy 3.9-3.10 (thanks @mgorny)
- Various linting and docs improvements (thanks @jairhenrique)
- Tornado fixes (thanks @graingert)
- 5.1.0
- Use ruff for linting (instead of current flake8/isort/pyflakes) - thanks @jairhenrique
- Enable rule B (flake8-bugbear) on ruff - thanks @jairhenrique
- Configure read the docs V2 - thanks @jairhenrique
- Fix typo in docs - thanks @quasimik
- Make json.loads of Python >=3.6 decode bytes by itself - thanks @hartwork
- Fix body matcher for chunked requests (fixes #734) - thanks @hartwork
- Fix query param filter for aiohttp (fixes #517) - thanks @hartwork and @salomvary
- Remove unnecessary dependency on six. - thanks @charettes
- build(deps): update sphinx requirement from <7 to <8 - thanks @jairhenrique
- Add action to validate docs - thanks @jairhenrique
- Add editorconfig file - thanks @jairhenrique
- Drop iscoroutinefunction fallback function for unsupported python thanks @jairhenrique
- 5.0.0
- BREAKING CHANGE: Drop support for Python 3.7. 3.7 is EOL as of 6/27/23 Thanks @jairhenrique
- BREAKING CHANGE: Custom Cassette persisters no longer catch ValueError. If you have implemented a custom persister (has anyone implemented a custom persister? Let us know!) then you will need to throw a CassetteNotFoundError when unable to find a cassette. See #681 for discussion and reason for this change. Thanks @amosjyng for the PR and the review from @hartwork
- 4.4.0
- HUGE thanks to @hartwork for all the work done on this release!
- Bring vcr/unittest in to vcrpy as a full feature of vcr instead of a separate library. Big thanks to @hartwork for doing this and to @agriffis for originally creating the library
- 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)
- 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 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

320
docs/conf.py Normal file
View File

@@ -0,0 +1,320 @@
#
# vcrpy documentation build configuration file, created by
# sphinx-quickstart on Sun Sep 13 11:18:00 2015.
#
# This file is execfile()d with the current directory set to its
# containing dir.
#
# Note that not all possible configuration values are present in this
# autogenerated file.
#
# 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.
# sys.path.insert(0, os.path.abspath('.'))
# -- General configuration ------------------------------------------------
# If your documentation needs a minimal Sphinx version, state it here.
# needs_sphinx = '1.0'
# 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",
"sphinx.ext.todo",
"sphinx.ext.githubpages",
]
# Add any paths that contain templates here, relative to this directory.
templates_path = ["_templates"]
# The suffix(es) of source filenames.
# You can specify multiple suffix as a list of string:
# source_suffix = ['.rst', '.md']
source_suffix = ".rst"
# The encoding of source files.
# source_encoding = 'utf-8-sig'
# The master toctree document.
master_doc = "index"
# General information about the project.
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"
# The full version, including alpha/beta/rc tags.
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 = "en"
# There are two options for replacing |today|: either, you set today to some
# non-false value, then it is used:
# today = ''
# Else, today_fmt is used as the format for a strftime call.
# today_fmt = '%B %d, %Y'
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
exclude_patterns = ["_build"]
# The reST default role (used for this markup: `text`) to use for all
# documents.
# default_role = None
# If true, '()' will be appended to :func: etc. cross-reference text.
# add_function_parentheses = True
# If true, the current module name will be prepended to all description
# unit titles (such as .. function::).
# add_module_names = True
# If true, sectionauthor and moduleauthor directives will be shown in the
# output. They are ignored by default.
# show_authors = False
# The name of the Pygments (syntax highlighting) style to use.
pygments_style = "sphinx"
# A list of ignored prefixes for module index sorting.
# modindex_common_prefix = []
# If true, keep warnings as "system message" paragraphs in the built documents.
# keep_warnings = False
# If true, `todo` and `todoList` produce output, else they produce nothing.
todo_include_todos = False
# -- Options for HTML output ----------------------------------------------
# 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()]
# 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
# documentation.
# html_theme_options = {}
# The name for this set of Sphinx documents. If None, it defaults to
# "<project> v<release> documentation".
# html_title = None
# A shorter title for the navigation bar. Default is the same as html_title.
# html_short_title = None
# The name of an image file (relative to this directory) to place at the top
# of the sidebar.
# html_logo = None
# The name of an image file (within the static path) to use as favicon of the
# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
# pixels large.
# html_favicon = None
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ["_static"]
# Add any extra paths that contain custom files (such as robots.txt or
# .htaccess) here, relative to this directory. These files are copied
# directly to the root of the documentation.
# html_extra_path = []
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
# using the given strftime format.
# html_last_updated_fmt = '%b %d, %Y'
# If true, SmartyPants will be used to convert quotes and dashes to
# typographically correct entities.
# html_use_smartypants = True
# 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.
# html_additional_pages = {}
# If false, no module index is generated.
# html_domain_indices = True
# If false, no index is generated.
# html_use_index = True
# If true, the index is split into individual pages for each letter.
# html_split_index = False
# If true, links to the reST sources are added to the pages.
# html_show_sourcelink = True
# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
# html_show_sphinx = True
# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
# html_show_copyright = True
# If true, an OpenSearch description file will be output, and all pages will
# contain a <link> tag referring to it. The value of this option must be the
# base URL from which the finished HTML is served.
# html_use_opensearch = ''
# This is the file name suffix for HTML files (e.g. ".xhtml").
# html_file_suffix = None
# Language to be used for generating the HTML full-text search index.
# Sphinx supports the following languages:
# 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja'
# 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr'
# html_search_language = 'en'
# A dictionary with options for the search language support, empty by default.
# Now only 'ja' uses this config value
# html_search_options = {'type': 'default'}
# The name of a javascript file (relative to the configuration directory) that
# implements a search results scorer. If empty, the default will be used.
# html_search_scorer = 'scorer.js'
# Output file base name for HTML help builder.
htmlhelp_basename = "vcrpydoc"
# -- Options for LaTeX output ---------------------------------------------
latex_elements = {
# The paper size ('letterpaper' or 'a4paper').
# 'papersize': 'letterpaper',
# The font size ('10pt', '11pt' or '12pt').
# 'pointsize': '10pt',
# Additional stuff for the LaTeX preamble.
# 'preamble': '',
# Latex figure (float) alignment
# '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", "vcrpy Documentation", "Kevin McCarthy", "manual"),
]
# The name of an image file (relative to this directory) to place at the top of
# the title page.
# latex_logo = None
# For "manual" documents, if this is true, then toplevel headings are parts,
# not chapters.
# latex_use_parts = False
# If true, show page references after internal links.
# latex_show_pagerefs = False
# If true, show URL addresses after external links.
# latex_show_urls = False
# Documents to append as an appendix to all manuals.
# latex_appendices = []
# If false, no module index is generated.
# latex_domain_indices = True
# -- Options for manual page output ---------------------------------------
# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [(master_doc, "vcrpy", "vcrpy Documentation", [author], 1)]
# If true, show URL addresses after external links.
# man_show_urls = False
# -- Options for Texinfo output -------------------------------------------
# Grouping the document tree into Texinfo files. List of tuples
# (source start file, target name, title, author,
# dir menu entry, description, category)
texinfo_documents = [
(
master_doc,
"vcrpy",
"vcrpy Documentation",
author,
"vcrpy",
"One line description of project.",
"Miscellaneous",
),
]
# Documents to append as an appendix to all manuals.
# texinfo_appendices = []
# If false, no module index is generated.
# texinfo_domain_indices = True
# How to display URL addresses: 'footnote', 'no', or 'inline'.
# texinfo_show_urls = 'footnote'
# If true, do not generate a @detailmenu in the "Top" node's menu.
# texinfo_no_detailmenu = False
# Example configuration for intersphinx: refer to the Python standard library.
intersphinx_mapping = {"python": ("https://docs.python.org/3", None)}
html_theme = "alabaster"

61
docs/configuration.rst Normal file
View File

@@ -0,0 +1,61 @@
Configuration
=============
If you don't like VCR's defaults, you can set options by instantiating a
``VCR`` class and setting the options on it.
.. code:: python
import vcr
my_vcr = vcr.VCR(
serializer='json',
cassette_library_dir='fixtures/cassettes',
record_mode='once',
match_on=['uri', 'method'],
)
with my_vcr.use_cassette('test.json'):
# your http code here
Otherwise, you can override options each time you use a cassette.
.. code:: python
with vcr.use_cassette('test.yml', serializer='json', record_mode='once'):
# your http code here
Note: Per-cassette overrides take precedence over the global config.
Request matching
----------------
Request matching is configurable and allows you to change which requests
VCR considers identical. The default behavior is
``['method', 'scheme', 'host', 'port', 'path', 'query']`` which means
that requests with both the same URL and method (ie POST or GET) are
considered identical.
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)
- 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)
- query (the query string of the request)
- raw\_body (the entire request body as is)
- body (the entire request body unmarshalled by content-type
i.e. xmlrpc, json, form-urlencoded, falling back on raw\_body)
- headers (the headers of the request)
Backwards compatible matchers:
- url (the ``uri`` alias)
If these options don't work for you, you can also register your own
request matcher. This is described in the Advanced section of this
README.

140
docs/contributing.rst Normal file
View File

@@ -0,0 +1,140 @@
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 `Github Actions CI <https://github.com/kevin1024/vcrpy/actions>`__,
but you can also run them yourself using `pytest <http://pytest.org/>`__.
In order for the boto3 tests to run, you will need an AWS key.
Refer to the `boto3
documentation <https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/index.html>`__
for how to set this up. I have marked the boto3 tests as optional in
Travis so you don't have to worry about them failing if you submit a
pull request.
Using Pyenv with VCR's test suite
---------------------------------
Pyenv is a tool for managing multiple installation of python on your system.
See the full documentation at their `github <https://github.com/pyenv/pyenv>`_
in this example::
git clone https://github.com/pyenv/pyenv ~/.pyenv
# Add ~/.pyenv/bin to your PATH
export PATH="$PATH:~/.pyenv/bin"
# Setup shim paths
eval "$(pyenv init -)"
# Install supported versions (at time of writing), this does not activate them
pyenv install 3.12.0 pypy3.10
# This activates them
pyenv local 3.12.0 pypy3.10
# Run the whole test suite
pip install .[tests]
./runtests.sh
Troubleshooting on MacOSX
-------------------------
If you have this kind of error when running tests :
.. code:: python
__main__.ConfigurationError: Curl is configured to use SSL, but we have
not been able to determine which SSL backend it is using. Please see PycURL documentation for how to specify the SSL backend manually.
Then you need to define some environment variables:
.. code:: bash
export PYCURL_SSL_LIBRARY=openssl
export LDFLAGS=-L/usr/local/opt/openssl/lib
export CPPFLAGS=-I/usr/local/opt/openssl/include
Reference : `stackoverflow issue <https://stackoverflow.com/questions/51019622/curl-is-configured-to-use-ssl-but-we-have-not-been-able-to-determine-which-ssl>`__

57
docs/debugging.rst Normal file
View File

@@ -0,0 +1,57 @@
Debugging
=========
VCR.py has a few log messages you can turn on to help you figure out if
HTTP requests are hitting a real server or not. You can turn them on
like this:
.. code:: python
import vcr
import requests
import logging
logging.basicConfig() # you need to initialize logging, otherwise you will not see anything from vcrpy
vcr_log = logging.getLogger("vcr")
vcr_log.setLevel(logging.INFO)
with vcr.use_cassette('headers.yml'):
requests.get('http://httpbin.org/headers')
The first time you run this, you will see::
INFO:vcr.stubs:<Request (GET) http://httpbin.org/headers> not in cassette, sending to real server
The second time, you will see::
INFO:vcr.stubs:Playing response for <Request (GET) http://httpbin.org/headers> from cassette
If you set the loglevel to DEBUG, you will also get information about
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.
The goal is to see which matcher(s) failed and understand what part of the failed request may have changed.
It can return multiple similar requests with :
- the matchers that have succeeded
- the matchers that have failed
- for each failed matchers, why it has failed with an assertion message
CannotOverwriteExistingCassetteException message example :
.. 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.
Found 1 similar requests with 1 different matchers :
1 - (<Request (GET) https://www.googleapis.com/?alt=json&maxResults=500>).
Matchers succeeded : ['method', 'scheme', 'host', 'port', 'path']
Matchers failed :
query - assertion failure :
[('alt', 'json'), ('maxResults', '200')] != [('alt', 'json'), ('maxResults', '500')]

25
docs/index.rst Normal file
View File

@@ -0,0 +1,25 @@
.. include:: ../README.rst
Contents
========
.. toctree::
:maxdepth: 3
installation
usage
configuration
advanced
api
debugging
contributing
changelog
==================
Indices and tables
==================
* :ref:`genindex`
* :ref:`modindex`
* :ref:`search`

86
docs/installation.rst Normal file
View File

@@ -0,0 +1,86 @@
Installation
============
VCR.py is a package on `PyPI <https://pypi.python.org>`__, so you can install
with pip::
pip3 install vcrpy
Compatibility
-------------
VCR.py supports Python 3.9+, and `pypy <http://pypy.org>`__.
The following HTTP libraries are supported:
- ``aiohttp``
- ``boto3``
- ``http.client``
- ``httplib2``
- ``requests`` (>=2.16.2 versions)
- ``tornado.httpclient``
- ``urllib2``
- ``urllib3``
- ``httpx``
- ``httpcore``
Speed
-----
VCR.py runs about 10x faster when `pyyaml <http://pyyaml.org>`__ can use the
`libyaml extensions <http://pyyaml.org/wiki/LibYAML>`__. In order for this to
work, libyaml needs to be available when pyyaml is built. Additionally the flag
is cached by pip, so you might need to explicitly avoid the cache when
rebuilding pyyaml.
1. Test if pyyaml is built with libyaml. This should work::
python3 -c 'from yaml import CLoader'
2. Install libyaml according to your Linux distribution, or using `Homebrew
<http://mxcl.github.com/homebrew/>`__ on Mac::
brew install libyaml # Mac with Homebrew
apt-get install libyaml-dev # Ubuntu
dnf install libyaml-devel # Fedora
3. Rebuild pyyaml with libyaml::
pip3 uninstall pyyaml
pip3 --no-cache-dir install pyyaml
Upgrade
-------
New Cassette Format
~~~~~~~~~~~~~~~~~~~
The cassette format has changed in *VCR.py 1.x*, the *VCR.py 0.x*
cassettes cannot be used with *VCR.py 1.x*. The easiest way to upgrade
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::
python3 -m vcr.migration PATH
The PATH can be either a path to the directory with cassettes or the
path to a single cassette.
*Note*: Back up your cassettes files before migration. The migration
*should* only modify cassettes using the old 0.x format.
New serializer / deserializer API
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
If you made a custom serializer, you will need to update it to match the
new API in version 1.0.x
- Serializers now take dicts and return strings.
- Deserializers take strings and return dicts (instead of requests,
responses pair)
Ruby VCR compatibility
----------------------
VCR.py does not aim to match the format of the Ruby VCR YAML files.
Cassettes generated by Ruby's VCR are not compatible with VCR.py.

2
docs/requirements.txt Normal file
View File

@@ -0,0 +1,2 @@
sphinx<9
sphinx_rtd_theme==3.0.2

170
docs/usage.rst Normal file
View File

@@ -0,0 +1,170 @@
Usage
=====
.. code:: python
import vcr
import urllib.request
with vcr.use_cassette('fixtures/vcr_cassettes/synopsis.yaml'):
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
replay the response from iana.org when the http request is made. This
test is now fast (no real HTTP requests are made anymore), deterministic
(the test will continue to pass, even if you are offline, or iana.org
goes down for maintenance) and accurate (the response will contain the
same headers and body you get from a real request).
You can also use VCR.py as a decorator. The same request above would
look like this:
.. code:: python
@vcr.use_cassette('fixtures/vcr_cassettes/synopsis.yaml')
def test_iana():
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.
.. code:: python
@vcr.use_cassette()
def test_iana():
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
which the test is defined. See the Automatic Test Naming section below
for more details.
Record Modes
------------
VCR supports 4 record modes (with the same behavior as Ruby's VCR):
once
~~~~
- Replay previously recorded interactions.
- Record new interactions if there is no cassette file.
- Cause an error to be raised for new requests if there is a cassette
file.
It is similar to the new\_episodes record mode, but will prevent new,
unexpected requests from being made (e.g. because the request URI
changed).
once is the default record mode, used when you do not set one.
new\_episodes
~~~~~~~~~~~~~
- Record new interactions.
- Replay previously recorded interactions. It is similar to the once
record mode, but will always record new interactions, even if you
have an existing recorded one that is similar, but not identical.
This was the default behavior in versions < 0.3.0
none
~~~~
- Replay previously recorded interactions.
- Cause an error to be raised for any new requests. This is useful when
your code makes potentially dangerous HTTP requests. The none record
mode guarantees that no new HTTP requests will be made.
all
~~~
- Record new interactions.
- Never replay previously recorded interactions. This can be
temporarily used to force VCR to re-record a cassette (i.e. to ensure
the responses are not out of date) or can be used when you simply
want to log all HTTP requests.
Unittest Integration
--------------------
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
------------------
A Pytest plugin is available here : `pytest-vcr
<https://github.com/ktosiek/pytest-vcr>`__.
Alternative plugin, that also provides network access blocking: `pytest-recording
<https://github.com/kiwicom/pytest-recording>`__.

32
pyproject.toml Normal file
View File

@@ -0,0 +1,32 @@
[tool.codespell]
skip = '.git,*.pdf,*.svg,.tox'
ignore-regex = "\\\\[fnrstv]"
[tool.pytest]
addopts = ["--strict-config", "--strict-markers"]
asyncio_default_fixture_loop_scope = "session"
asyncio_default_test_loop_scope = "session"
markers = ["online"]
[tool.ruff]
line-length = 110
target-version = "py310"
[tool.ruff.lint]
select = [
"B", # flake8-bugbear
"C4", # flake8-comprehensions
"COM", # flake8-commas
"E", # pycodestyle error
"F", # pyflakes
"I", # isort
"ISC", # flake8-implicit-str-concat
"PIE", # flake8-pie
"RUF", # Ruff-specific rules
"UP", # pyupgrade
"W", # pycodestyle warning
"SIM",
]
[tool.ruff.lint.isort]
known-first-party = ["vcr"]

5
runtests.sh Executable file
View File

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

2
setup.cfg Normal file
View File

@@ -0,0 +1,2 @@
[bdist_wheel]
universal=1

127
setup.py
View File

@@ -1,58 +1,89 @@
#!/usr/bin/env python
import sys
from setuptools import setup
from setuptools.command.test import test as TestCommand
import codecs
import os
import re
from pathlib import Path
from setuptools import find_packages, setup
long_description = Path("README.rst").read_text()
here = os.path.abspath(os.path.dirname(__file__))
class PyTest(TestCommand):
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 finalize_options(self):
TestCommand.finalize_options(self)
self.test_args = []
self.test_suite = True
def run_tests(self):
# import here, cause outside the eggs aren't loaded
import pytest
errno = pytest.main(self.test_args)
sys.exit(errno)
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.")
install_requires = [
"PyYAML",
"wrapt",
]
extras_require = {
"tests": [
"aiohttp",
"boto3",
"cryptography",
"httpbin",
"httpcore",
"httplib2",
"httpx",
"pycurl; platform_python_implementation !='PyPy'",
"pytest",
"pytest-aiohttp",
"pytest-asyncio",
"pytest-cov",
"pytest-httpbin",
"requests>=2.22.0",
"tornado",
"urllib3",
"werkzeug==2.0.3",
],
}
setup(
name='vcrpy',
version='1.0.1',
description=(
"Automatically mock your HTTP interactions to simplify and "
"speed up testing"
),
author='Kevin McCarthy',
author_email='me@kevinmccarthy.org',
url='https://github.com/kevin1024/vcrpy',
packages=[
'vcr',
'vcr.stubs',
'vcr.compat',
'vcr.persisters',
'vcr.serializers',
],
package_dir={
'vcr': 'vcr',
'vcr.stubs': 'vcr/stubs',
'vcr.compat': 'vcr/compat',
'vcr.persisters': 'vcr/persisters',
},
install_requires=['PyYAML', 'contextdecorator', 'six'],
license='MIT',
tests_require=['pytest', 'mock', 'pytest-localserver'],
cmdclass={'test': PyTest},
name="vcrpy",
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=["tests*"]),
python_requires=">=3.10",
install_requires=install_requires,
license="MIT",
extras_require=extras_require,
tests_require=extras_require["tests"],
classifiers=[
'Development Status :: 4 - Beta',
'Environment :: Console',
'Intended Audience :: Developers',
'Programming Language :: Python',
'Programming Language :: Python :: 3',
'Topic :: Software Development :: Testing',
'Topic :: Internet :: WWW/HTTP',
'License :: OSI Approved :: MIT License',
],
"Development Status :: 5 - Production/Stable",
"Environment :: Console",
"Intended Audience :: Developers",
"Programming Language :: Python",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3 :: Only",
"Programming Language :: Python :: Implementation :: CPython",
"Programming Language :: Python :: Implementation :: PyPy",
"Topic :: Software Development :: Testing",
"Topic :: Internet :: WWW/HTTP",
"License :: OSI Approved :: MIT License",
],
)

View File

@@ -11,9 +11,12 @@ 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'))
except Exception:
assert False
json.loads(b)
except Exception as error:
raise AssertionError() from error
assert True

View File

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

View File

@@ -2,14 +2,14 @@ version: 1
interactions:
- request:
body: null
headers:
headers:
accept: ['*/*']
accept-encoding: ['gzip, deflate, compress']
user-agent: ['python-requests/2.2.1 CPython/2.6.1 Darwin/10.8.0']
method: GET
uri: http://httpbin.org/ip
response:
body: {string: !!python/unicode "{\n \"origin\": \"217.122.164.194\"\n}"}
body: {string: "{\n \"origin\": \"217.122.164.194\"\n}"}
headers:
access-control-allow-origin: ['*']
content-type: [application/json]

View File

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

View File

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

View File

@@ -0,0 +1,42 @@
# flake8: noqa
import asyncio
import aiohttp
async def aiohttp_request(loop, method, url, output="text", encoding="utf-8", content_type=None, **kwargs):
async with aiohttp.ClientSession(loop=loop) as session:
response_ctx = session.request(method, url, **kwargs)
response = await response_ctx.__aenter__()
if output == "text":
content = await response.text()
elif output == "json":
content_type = content_type or "application/json"
content = await response.json(encoding=encoding, content_type=content_type)
elif output == "raw":
content = await response.read()
elif output == "stream":
content = await response.content.read()
response_ctx._resp.close()
await session.close()
return response, content
def aiohttp_app():
async def hello(request):
return aiohttp.web.Response(text="hello")
async def json(request):
return aiohttp.web.json_response({})
async def json_empty_body(request):
return aiohttp.web.json_response()
app = aiohttp.web.Application()
app.router.add_get("/", hello)
app.router.add_get("/json", json)
app.router.add_get("/json/empty", json_empty_body)
return app

View File

@@ -0,0 +1,41 @@
interactions:
- request:
body: ''
headers:
accept:
- '*/*'
accept-encoding:
- gzip, deflate, br
connection:
- keep-alive
host:
- httpbin.org
user-agent:
- python-httpx/0.23.0
method: GET
uri: https://httpbin.org/gzip
response:
content: "{\n \"gzipped\": true, \n \"headers\": {\n \"Accept\": \"*/*\",
\n \"Accept-Encoding\": \"gzip, deflate, br\", \n \"Host\": \"httpbin.org\",
\n \"User-Agent\": \"python-httpx/0.23.0\", \n \"X-Amzn-Trace-Id\": \"Root=1-62a62a8d-5f39b5c50c744da821d6ea99\"\n
\ }, \n \"method\": \"GET\", \n \"origin\": \"146.200.25.115\"\n}\n"
headers:
Access-Control-Allow-Credentials:
- 'true'
Access-Control-Allow-Origin:
- '*'
Connection:
- keep-alive
Content-Encoding:
- gzip
Content-Length:
- '230'
Content-Type:
- application/json
Date:
- Sun, 12 Jun 2022 18:03:57 GMT
Server:
- gunicorn/19.9.0
http_version: HTTP/1.1
status_code: 200
version: 1

View File

@@ -0,0 +1,42 @@
interactions:
- request:
body: null
headers:
Accept:
- '*/*'
Accept-Encoding:
- gzip, deflate, br
Connection:
- keep-alive
User-Agent:
- python-requests/2.28.0
method: GET
uri: https://httpbin.org/gzip
response:
body:
string: !!binary |
H4sIAKwrpmIA/z2OSwrCMBCG956izLIkfQSxkl2RogfQA9R2bIM1iUkqaOndnYDIrGa+/zELDB9l
LfYgg5uRwYhtj86DXKDuOrQBJKR5Cuy38kZ3pld6oHu0sqTH29QGZMnVkepgtMYuKKNJcEe0vJ3U
C4mcjI9hpaiygqaUW7ETFYGLR8frAXXE9h1Go7nD54w++FxkYp8VsDJ4IBH6E47NmVzGqUHFkn8g
rJsvp2omYs8AAAA=
headers:
Access-Control-Allow-Credentials:
- 'true'
Access-Control-Allow-Origin:
- '*'
Connection:
- Close
Content-Encoding:
- gzip
Content-Length:
- '182'
Content-Type:
- application/json
Date:
- Sun, 12 Jun 2022 18:08:44 GMT
Server:
- Pytest-HTTPBIN/0.1.0
status:
code: 200
message: great
version: 1

View File

@@ -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,464 @@
import logging
import ssl
import urllib.parse
import pytest
import pytest_httpbin.certs
import yarl
import vcr
asyncio = pytest.importorskip("asyncio")
aiohttp = pytest.importorskip("aiohttp")
from .aiohttp_utils import aiohttp_app, aiohttp_request # noqa: E402
HTTPBIN_SSL_CONTEXT = ssl.create_default_context(cafile=pytest_httpbin.certs.where())
def run_in_loop(fn):
async def wrapper():
return await fn(asyncio.get_running_loop())
return asyncio.run(wrapper())
def request(method, url, output="text", **kwargs):
def run(loop):
return aiohttp_request(loop, method, url, output=output, **kwargs)
return run_in_loop(run)
def get(url, output="text", **kwargs):
return request("GET", url, output=output, **kwargs)
def post(url, output="text", **kwargs):
return request("POST", url, output="text", **kwargs)
@pytest.mark.online
def test_status(tmpdir, httpbin):
url = httpbin.url
with vcr.use_cassette(str(tmpdir.join("status.yaml"))):
response, _ = get(url)
with vcr.use_cassette(str(tmpdir.join("status.yaml"))) as cassette:
cassette_response, _ = get(url)
assert cassette_response.status == response.status
assert cassette.play_count == 1
@pytest.mark.online
@pytest.mark.parametrize("auth", [None, aiohttp.BasicAuth("vcrpy", "test")])
def test_headers(tmpdir, auth, httpbin):
url = httpbin.url
with vcr.use_cassette(str(tmpdir.join("headers.yaml"))):
response, _ = get(url, auth=auth)
with vcr.use_cassette(str(tmpdir.join("headers.yaml"))) as cassette:
if auth is not None:
request = cassette.requests[0]
assert "AUTHORIZATION" in request.headers
cassette_response, _ = get(url, auth=auth)
assert cassette_response.headers.items() == response.headers.items()
assert cassette.play_count == 1
assert "istr" not in cassette.data[0]
assert "yarl.URL" not in cassette.data[0]
@pytest.mark.online
def test_case_insensitive_headers(tmpdir, httpbin):
url = httpbin.url
with vcr.use_cassette(str(tmpdir.join("whatever.yaml"))):
_, _ = get(url)
with vcr.use_cassette(str(tmpdir.join("whatever.yaml"))) as cassette:
cassette_response, _ = 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_text(tmpdir, httpbin):
url = httpbin.url
with vcr.use_cassette(str(tmpdir.join("text.yaml"))):
_, response_text = get(url)
with vcr.use_cassette(str(tmpdir.join("text.yaml"))) as cassette:
_, cassette_response_text = get(url)
assert cassette_response_text == response_text
assert cassette.play_count == 1
@pytest.mark.online
def test_json(tmpdir, httpbin):
url = httpbin.url + "/json"
headers = {"Content-Type": "application/json"}
with vcr.use_cassette(str(tmpdir.join("json.yaml"))):
_, response_json = get(url, output="json", headers=headers)
with vcr.use_cassette(str(tmpdir.join("json.yaml"))) as cassette:
_, cassette_response_json = get(url, output="json", headers=headers)
assert cassette_response_json == response_json
assert cassette.play_count == 1
@pytest.mark.online
def test_binary(tmpdir, httpbin):
url = httpbin.url + "/image/png"
with vcr.use_cassette(str(tmpdir.join("binary.yaml"))):
_, response_binary = get(url, output="raw")
with vcr.use_cassette(str(tmpdir.join("binary.yaml"))) as cassette:
_, cassette_response_binary = get(url, output="raw")
assert cassette_response_binary == response_binary
assert cassette.play_count == 1
@pytest.mark.online
def test_stream(tmpdir, httpbin):
url = httpbin.url
with vcr.use_cassette(str(tmpdir.join("stream.yaml"))):
_, body = get(url, output="raw") # Do not use stream here, as the stream is exhausted by vcr
with vcr.use_cassette(str(tmpdir.join("stream.yaml"))) as cassette:
_, 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, body, caplog, httpbin):
caplog.set_level(logging.INFO)
data = {"key1": "value1", "key2": "value2"}
url = httpbin.url
with vcr.use_cassette(str(tmpdir.join("post.yaml"))):
_, response_json = post(url, **{body: data})
with vcr.use_cassette(str(tmpdir.join("post.yaml"))) as cassette:
request = cassette.requests[0]
assert request.body == data
_, cassette_response_json = post(url, **{body: data})
assert cassette_response_json == response_json
assert cassette.play_count == 1
assert next(
(
log
for log in caplog.records
if log.getMessage() == f"<Request (POST) {url}> not in cassette, sending to real server"
),
None,
), "Log message not found."
@pytest.mark.online
def test_params(tmpdir, httpbin):
url = httpbin.url + "/get?d=d"
headers = {"Content-Type": "application/json"}
params = {"a": 1, "b": 2, "c": "c"}
with vcr.use_cassette(str(tmpdir.join("get.yaml"))) as cassette:
_, response_json = get(url, output="json", params=params, headers=headers)
assert response_json["args"] == {"a": "1", "b": "2", "c": "c", "d": "d"}
with vcr.use_cassette(str(tmpdir.join("get.yaml"))) as cassette:
_, cassette_response_json = get(url, output="json", params=params, headers=headers)
assert cassette_response_json == response_json
assert cassette.play_count == 1
@pytest.mark.online
def test_params_same_url_distinct_params(tmpdir, httpbin):
url = httpbin.url + "/json"
headers = {"Content-Type": "application/json"}
params = {"a": 1, "b": 2, "c": "c"}
with vcr.use_cassette(str(tmpdir.join("get.yaml"))) as cassette:
_, response_json = get(url, output="json", params=params, headers=headers)
with vcr.use_cassette(str(tmpdir.join("get.yaml"))) as cassette:
_, cassette_response_json = get(url, output="json", params=params, headers=headers)
assert cassette_response_json == response_json
assert cassette.play_count == 1
other_params = {"other": "params"}
with (
vcr.use_cassette(str(tmpdir.join("get.yaml"))) as cassette,
pytest.raises(vcr.errors.CannotOverwriteExistingCassetteException),
):
get(url, output="text", params=other_params)
@pytest.mark.online
def test_params_on_url(tmpdir, httpbin):
url = httpbin.url + "/get?a=1&b=foo"
headers = {"Content-Type": "application/json"}
with vcr.use_cassette(str(tmpdir.join("get.yaml"))) as cassette:
_, response_json = get(url, output="json", headers=headers)
request = cassette.requests[0]
assert request.url == url
with vcr.use_cassette(str(tmpdir.join("get.yaml"))) as cassette:
_, cassette_response_json = get(url, output="json", headers=headers)
request = cassette.requests[0]
assert request.url == url
assert cassette_response_json == response_json
assert cassette.play_count == 1
def test_aiohttp_test_client(aiohttp_client, tmpdir):
loop = asyncio.get_event_loop()
app = aiohttp_app()
url = "/"
client = loop.run_until_complete(aiohttp_client(app))
with vcr.use_cassette(str(tmpdir.join("get.yaml"))):
response = loop.run_until_complete(client.get(url))
assert response.status == 200
response_text = loop.run_until_complete(response.text())
assert response_text == "hello"
response_text = loop.run_until_complete(response.text(errors="replace"))
assert response_text == "hello"
with vcr.use_cassette(str(tmpdir.join("get.yaml"))) as cassette:
response = loop.run_until_complete(client.get(url))
request = cassette.requests[0]
assert request.url == str(client.make_url(url))
response_text = loop.run_until_complete(response.text())
assert response_text == "hello"
assert cassette.play_count == 1
def test_aiohttp_test_client_json(aiohttp_client, tmpdir):
loop = asyncio.get_event_loop()
app = aiohttp_app()
url = "/json/empty"
client = loop.run_until_complete(aiohttp_client(app))
with vcr.use_cassette(str(tmpdir.join("get.yaml"))):
response = loop.run_until_complete(client.get(url))
assert response.status == 200
response_json = loop.run_until_complete(response.json())
assert response_json is None
with vcr.use_cassette(str(tmpdir.join("get.yaml"))) as cassette:
response = loop.run_until_complete(client.get(url))
request = cassette.requests[0]
assert request.url == str(client.make_url(url))
response_json = loop.run_until_complete(response.json())
assert response_json is None
assert cassette.play_count == 1
@pytest.mark.online
def test_redirect(tmpdir, httpbin):
url = httpbin.url + "/redirect/2"
with vcr.use_cassette(str(tmpdir.join("redirect.yaml"))):
response, _ = get(url)
with vcr.use_cassette(str(tmpdir.join("redirect.yaml"))) as cassette:
cassette_response, _ = get(url)
assert cassette_response.status == response.status
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_info.url == response.request_info.url
assert cassette_response.request_info.method == response.request_info.method
assert cassette_response.request_info.headers.items() == response.request_info.headers.items()
assert cassette_response.request_info.real_url == response.request_info.real_url
@pytest.mark.online
def test_not_modified(tmpdir, httpbin):
"""It doesn't try to redirect on 304"""
url = httpbin.url + "/status/304"
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, httpbin):
"""We should capture, record, and replay all requests and response chains,
even if there are duplicate ones.
We should replay in the order we saw them.
"""
url = httpbin.url
with vcr.use_cassette(str(tmpdir.join("text.yaml"))):
_, response_text1 = get(url, output="text")
_, response_text2 = get(url, output="text")
with vcr.use_cassette(str(tmpdir.join("text.yaml"))) as cassette:
resp, cassette_response_text = get(url, output="text")
assert resp.status == 200
assert cassette_response_text == response_text1
# We made only one request, so we should only play 1 recording.
assert cassette.play_count == 1
# Now make the second test to url
resp, cassette_response_text = get(url, output="text")
assert resp.status == 200
assert cassette_response_text == response_text2
# Now that we made both requests, we should have played both.
assert cassette.play_count == 2
def test_cookies(httpbin_both, 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, 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(yarl.URL(cookies_url))
assert cookies["Cookie_1"].value == "Val_1"
assert cassette.play_count == 0
assert 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(yarl.URL(cookies_url))
assert cookies["Cookie_1"].value == "Val_1"
assert cassette.play_count == 2
assert cassette.requests[1].headers["Cookie"] == "Cookie_1=Val_1"
# Assert that it's ignoring expiration date
with vcr.use_cassette(tmp, record_mode=vcr.mode.NONE) as cassette:
cassette.responses[0]["headers"]["set-cookie"] = [
"Cookie_1=Val_1; Expires=Wed, 21 Oct 2015 07:28:00 GMT",
]
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(yarl.URL(cookies_url))
assert cookies["Cookie_1"].value == "Val_1"
run_in_loop(run)
@pytest.mark.online
def test_not_allow_redirects(tmpdir, httpbin):
url = httpbin + "/redirect-to?url=.%2F&status_code=308"
path = str(tmpdir.join("redirects.yaml"))
with vcr.use_cassette(path):
response, _ = get(url, allow_redirects=False)
assert response.url.path == "/redirect-to"
assert response.status == 308
with vcr.use_cassette(path) as cassette:
response, _ = get(url, allow_redirects=False)
assert response.url.path == "/redirect-to"
assert response.status == 308
assert cassette.play_count == 1
def test_filter_query_parameters(tmpdir, httpbin):
url = httpbin + "?password=secret"
path = str(tmpdir.join("query_param_filter.yaml"))
with vcr.use_cassette(path, filter_query_parameters=["password"]) as cassette:
get(url)
assert "password" not in cassette.requests[0].url
assert "secret" not in cassette.requests[0].url
with open(path) as f:
cassette_content = f.read()
assert "password" not in cassette_content
assert "secret" not in cassette_content

View File

@@ -1,100 +1,88 @@
'''Basic tests about cassettes'''
# 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
def test_nonexistent_directory(tmpdir):
'''If we load a cassette in a nonexistent directory, it can save ok'''
# Check to make sure directory doesnt exist
assert not os.path.exists(str(tmpdir.join('nonexistent')))
def test_nonexistent_directory(tmpdir, httpbin):
"""If we load a cassette in a nonexistent directory, it can save ok"""
# 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
with vcr.use_cassette(str(tmpdir.join('nonexistent', 'cassette.yml'))):
urlopen('http://httpbin.org/').read()
with vcr.use_cassette(str(tmpdir.join("nonexistent", "cassette.yml"))):
urlopen(httpbin.url).read()
# This should have made the file and the directory
assert os.path.exists(str(tmpdir.join('nonexistent', 'cassette.yml')))
assert os.path.exists(str(tmpdir.join("nonexistent", "cassette.yml")))
def test_unpatch(tmpdir):
'''Ensure that our cassette gets unpatched when we're done'''
with vcr.use_cassette(str(tmpdir.join('unpatch.yaml'))) as cass:
urlopen('http://httpbin.org/').read()
def test_unpatch(tmpdir, httpbin):
"""Ensure that our cassette gets unpatched when we're done"""
with vcr.use_cassette(str(tmpdir.join("unpatch.yaml"))) as cass:
urlopen(httpbin.url).read()
# Make the same request, and assert that we haven't served any more
# requests out of cache
urlopen('http://httpbin.org/').read()
urlopen(httpbin.url).read()
assert cass.play_count == 0
def test_basic_use(tmpdir):
'''
Copied from the docs
'''
with vcr.use_cassette('fixtures/vcr_cassettes/synopsis.yaml'):
response = urlopen(
'http://www.iana.org/domains/reserved'
).read()
assert b'Example domains' in response
def test_basic_json_use(tmpdir):
'''
def test_basic_json_use(tmpdir, httpbin):
"""
Ensure you can load a json serialized cassette
'''
test_fixture = 'fixtures/vcr_cassettes/synopsis.json'
with vcr.use_cassette(test_fixture, serializer='json'):
response = urlopen('http://httpbin.org/').read()
assert b'difficult sometimes' in response
"""
test_fixture = str(tmpdir.join("synopsis.json"))
with vcr.use_cassette(test_fixture, serializer="json"):
response = urlopen(httpbin.url).read()
assert b"HTTP Request &amp; Response Service" in response
def test_patched_content(tmpdir):
'''
def test_patched_content(tmpdir, httpbin):
"""
Ensure that what you pull from a cassette is what came from the
request
'''
with vcr.use_cassette(str(tmpdir.join('synopsis.yaml'))) as cass:
response = urlopen('http://httpbin.org/').read()
"""
with vcr.use_cassette(str(tmpdir.join("synopsis.yaml"))) as cass:
response = urlopen(httpbin.url).read()
assert cass.play_count == 0
with vcr.use_cassette(str(tmpdir.join('synopsis.yaml'))) as cass:
response2 = urlopen('http://httpbin.org/').read()
with vcr.use_cassette(str(tmpdir.join("synopsis.yaml"))) as cass:
response2 = urlopen(httpbin.url).read()
assert cass.play_count == 1
cass._save(force=True)
with vcr.use_cassette(str(tmpdir.join('synopsis.yaml'))) as cass:
response3 = urlopen('http://httpbin.org/').read()
with vcr.use_cassette(str(tmpdir.join("synopsis.yaml"))) as cass:
response3 = urlopen(httpbin.url).read()
assert cass.play_count == 1
assert response == response2
assert response2 == response3
def test_patched_content_json(tmpdir):
'''
def test_patched_content_json(tmpdir, httpbin):
"""
Ensure that what you pull from a json cassette is what came from the
request
'''
"""
testfile = str(tmpdir.join('synopsis.json'))
testfile = str(tmpdir.join("synopsis.json"))
with vcr.use_cassette(testfile) as cass:
response = urlopen('http://httpbin.org/').read()
response = urlopen(httpbin.url).read()
assert cass.play_count == 0
with vcr.use_cassette(testfile) as cass:
response2 = urlopen('http://httpbin.org/').read()
response2 = urlopen(httpbin.url).read()
assert cass.play_count == 1
cass._save(force=True)
with vcr.use_cassette(testfile) as cass:
response3 = urlopen('http://httpbin.org/').read()
response3 = urlopen(httpbin.url).read()
assert cass.play_count == 1
assert response == response2

View File

@@ -1,72 +0,0 @@
import pytest
boto = pytest.importorskip("boto")
import boto
import boto.iam
from boto.s3.connection import S3Connection
from boto.s3.key import Key
from ConfigParser import DuplicateSectionError
import vcr
def test_boto_stubs(tmpdir):
with vcr.use_cassette(str(tmpdir.join('boto-stubs.yml'))):
# Perform the imports within the patched context so that
# CertValidatingHTTPSConnection refers to the patched version.
from boto.https_connection import CertValidatingHTTPSConnection
from vcr.stubs.boto_stubs import VCRCertValidatingHTTPSConnection
# Prove that the class was patched by the stub and that we can instantiate it.
assert CertValidatingHTTPSConnection is VCRCertValidatingHTTPSConnection
CertValidatingHTTPSConnection('hostname.does.not.matter')
def test_boto_without_vcr():
s3_conn = S3Connection()
s3_bucket = s3_conn.get_bucket('boto-demo-1394171994') # a bucket you can access
k = Key(s3_bucket)
k.key = 'test.txt'
k.set_contents_from_string('hello world i am a string')
def test_boto_medium_difficulty(tmpdir):
s3_conn = S3Connection()
s3_bucket = s3_conn.get_bucket('boto-demo-1394171994') # a bucket you can access
with vcr.use_cassette(str(tmpdir.join('boto-medium.yml'))) as cass:
k = Key(s3_bucket)
k.key = 'test.txt'
k.set_contents_from_string('hello world i am a string')
with vcr.use_cassette(str(tmpdir.join('boto-medium.yml'))) as cass:
k = Key(s3_bucket)
k.key = 'test.txt'
k.set_contents_from_string('hello world i am a string')
def test_boto_hardcore_mode(tmpdir):
with vcr.use_cassette(str(tmpdir.join('boto-hardcore.yml'))) as cass:
s3_conn = S3Connection()
s3_bucket = s3_conn.get_bucket('boto-demo-1394171994') # a bucket you can access
k = Key(s3_bucket)
k.key = 'test.txt'
k.set_contents_from_string('hello world i am a string')
with vcr.use_cassette(str(tmpdir.join('boto-hardcore.yml'))) as cass:
s3_conn = S3Connection()
s3_bucket = s3_conn.get_bucket('boto-demo-1394171994') # a bucket you can access
k = Key(s3_bucket)
k.key = 'test.txt'
k.set_contents_from_string('hello world i am a string')
def test_boto_iam(tmpdir):
try:
boto.config.add_section('Boto')
except DuplicateSectionError:
pass
# Ensure that boto uses HTTPS
boto.config.set('Boto', 'is_secure', 'true')
# Ensure that boto uses CertValidatingHTTPSConnection
boto.config.set('Boto', 'https_validate_certificates', 'true')
with vcr.use_cassette(str(tmpdir.join('boto-iam.yml'))) as cass:
iam_conn = boto.iam.connect_to_region('universal')
iam_conn.get_all_users()
with vcr.use_cassette(str(tmpdir.join('boto-iam.yml'))) as cass:
iam_conn = boto.iam.connect_to_region('universal')
iam_conn.get_all_users()

View File

@@ -0,0 +1,104 @@
import os
import pytest
import vcr
boto3 = pytest.importorskip("boto3")
import botocore # noqa
try:
from botocore import awsrequest # noqa
botocore_awsrequest = True
except ImportError:
botocore_awsrequest = False
# skip tests if boto does not use vendored requests anymore
# https://github.com/boto/botocore/pull/1495
boto3_skip_vendored_requests = pytest.mark.skipif(
botocore_awsrequest,
reason=f"botocore version {botocore.__version__} does not use vendored requests anymore.",
)
boto3_skip_awsrequest = pytest.mark.skipif(
not botocore_awsrequest,
reason=f"botocore version {botocore.__version__} still uses vendored requests.",
)
IAM_USER_NAME = "vcrpy"
@pytest.fixture
def iam_client():
def _iam_client(boto3_session=None):
if boto3_session is None:
boto3_session = boto3.Session(
aws_access_key_id=os.environ.get("AWS_ACCESS_KEY_ID", "default"),
aws_secret_access_key=os.environ.get("AWS_SECRET_ACCESS_KEY", "default"),
aws_session_token=None,
region_name=os.environ.get("AWS_DEFAULT_REGION", "default"),
)
return boto3_session.client("iam")
return _iam_client
@pytest.fixture
def get_user(iam_client):
def _get_user(client=None, user_name=IAM_USER_NAME):
if client is None:
# Default client set with fixture `iam_client`
client = iam_client()
return client.get_user(UserName=user_name)
return _get_user
@pytest.mark.skipif(
os.environ.get("TRAVIS_PULL_REQUEST") != "false",
reason="Encrypted Environment Variables from Travis Repository Settings"
" are disabled on PRs from forks. "
"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
with vcr.use_cassette(str(tmpdir.join("boto3-medium.yml"))) as cass:
response = get_user()
assert response["User"]["UserName"] == IAM_USER_NAME
assert cass.all_played
@pytest.mark.skipif(
os.environ.get("TRAVIS_PULL_REQUEST") != "false",
reason="Encrypted Environment Variables from Travis Repository Settings"
" are disabled on PRs from forks. "
"https://docs.travis-ci.com/user/pull-requests/#pull-requests-and-security-restrictions",
)
def test_boto_hardcore_mode(tmpdir, iam_client, get_user):
with vcr.use_cassette(str(tmpdir.join("boto3-hardcore.yml"))):
ses = boto3.Session(
aws_access_key_id=os.environ.get("AWS_ACCESS_KEY_ID"),
aws_secret_access_key=os.environ.get("AWS_SECRET_ACCESS_KEY"),
region_name=os.environ.get("AWS_DEFAULT_REGION"),
)
client = iam_client(ses)
response = get_user(client=client)
assert response["User"]["UserName"] == IAM_USER_NAME
with vcr.use_cassette(str(tmpdir.join("boto3-hardcore.yml"))) as cass:
ses = boto3.Session(
aws_access_key_id=os.environ.get("AWS_ACCESS_KEY_ID"),
aws_secret_access_key=os.environ.get("AWS_SECRET_ACCESS_KEY"),
aws_session_token=None,
region_name=os.environ.get("AWS_DEFAULT_REGION"),
)
client = iam_client(ses)
response = get_user(client=client)
assert response["User"]["UserName"] == IAM_USER_NAME
assert cass.all_played

View File

@@ -1,50 +1,59 @@
import os
import json
import os
from urllib.request import urlopen
import pytest
import vcr
from six.moves.urllib.request import urlopen
from vcr.cassette import Cassette
def test_set_serializer_default_config(tmpdir):
my_vcr = vcr.VCR(serializer='json')
@pytest.mark.online
def test_set_serializer_default_config(tmpdir, httpbin):
my_vcr = vcr.VCR(serializer="json")
with my_vcr.use_cassette(str(tmpdir.join('test.json'))):
assert my_vcr.serializer == 'json'
urlopen('http://httpbin.org/get')
with my_vcr.use_cassette(str(tmpdir.join("test.json"))):
assert my_vcr.serializer == "json"
urlopen(httpbin.url)
with open(str(tmpdir.join('test.json'))) as f:
assert json.loads(f.read())
with open(str(tmpdir.join("test.json"))) as f:
file_content = f.read()
assert file_content.endswith("\n")
assert json.loads(file_content)
def test_default_set_cassette_library_dir(tmpdir):
my_vcr = vcr.VCR(cassette_library_dir=str(tmpdir.join('subdir')))
@pytest.mark.online
def test_default_set_cassette_library_dir(tmpdir, httpbin):
my_vcr = vcr.VCR(cassette_library_dir=str(tmpdir.join("subdir")))
with my_vcr.use_cassette('test.json'):
urlopen('http://httpbin.org/get')
with my_vcr.use_cassette("test.json"):
urlopen(httpbin.url)
assert os.path.exists(str(tmpdir.join('subdir').join('test.json')))
assert os.path.exists(str(tmpdir.join("subdir").join("test.json")))
def test_override_set_cassette_library_dir(tmpdir):
my_vcr = vcr.VCR(cassette_library_dir=str(tmpdir.join('subdir')))
@pytest.mark.online
def test_override_set_cassette_library_dir(tmpdir, httpbin):
my_vcr = vcr.VCR(cassette_library_dir=str(tmpdir.join("subdir")))
cld = str(tmpdir.join('subdir2'))
cld = str(tmpdir.join("subdir2"))
with my_vcr.use_cassette('test.json', cassette_library_dir=cld):
urlopen('http://httpbin.org/get')
with my_vcr.use_cassette("test.json", cassette_library_dir=cld):
urlopen(httpbin.url)
assert os.path.exists(str(tmpdir.join('subdir2').join('test.json')))
assert not os.path.exists(str(tmpdir.join('subdir').join('test.json')))
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):
my_vcr = vcr.VCR(match_on=['method'])
@pytest.mark.online
def test_override_match_on(tmpdir, httpbin):
my_vcr = vcr.VCR(match_on=["method"])
with my_vcr.use_cassette(str(tmpdir.join('test.json'))):
urlopen('http://httpbin.org/')
with my_vcr.use_cassette(str(tmpdir.join("test.json"))):
urlopen(httpbin.url)
with my_vcr.use_cassette(str(tmpdir.join('test.json'))) as cass:
urlopen('http://httpbin.org/get')
with my_vcr.use_cassette(str(tmpdir.join("test.json"))) as cass:
urlopen(httpbin.url)
assert len(cass) == 1
assert cass.play_count == 1
@@ -53,6 +62,43 @@ def test_override_match_on(tmpdir):
def test_missing_matcher():
my_vcr = vcr.VCR()
my_vcr.register_matcher("awesome", object)
with pytest.raises(KeyError):
with my_vcr.use_cassette("test.yaml", match_on=['notawesome']):
pass
with pytest.raises(KeyError), my_vcr.use_cassette("test.yaml", match_on=["notawesome"]):
pass
@pytest.mark.online
def test_dont_record_on_exception(tmpdir, httpbin):
my_vcr = vcr.VCR(record_on_exception=False)
@my_vcr.use_cassette(str(tmpdir.join("dontsave.yml")))
def some_test():
assert b"Not in content" in urlopen(httpbin.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), my_vcr.use_cassette(str(tmpdir.join("dontsave2.yml"))):
assert b"Not in content" in urlopen(httpbin.url).read()
assert not os.path.exists(str(tmpdir.join("dontsave2.yml")))
def test_set_drop_unused_requests(tmpdir, httpbin):
my_vcr = vcr.VCR(drop_unused_requests=True)
file = str(tmpdir.join("test.yaml"))
with my_vcr.use_cassette(file):
urlopen(httpbin.url)
urlopen(httpbin.url + "/get")
cassette = Cassette.load(path=file)
assert len(cassette) == 2
with my_vcr.use_cassette(file):
urlopen(httpbin.url)
cassette = Cassette.load(path=file)
assert len(cassette) == 1

View File

@@ -1,28 +1,30 @@
'''Basic tests about save behavior'''
# 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):
'''
@pytest.mark.online
def test_disk_saver_nowrite(tmpdir, httpbin):
"""
Ensure that when you close a cassette without changing it it doesn't
rewrite the file
'''
fname = str(tmpdir.join('synopsis.yaml'))
"""
fname = str(tmpdir.join("synopsis.yaml"))
with vcr.use_cassette(fname) as cass:
urlopen('http://www.iana.org/domains/reserved').read()
urlopen(httpbin.url).read()
assert cass.play_count == 0
last_mod = os.path.getmtime(fname)
with vcr.use_cassette(fname) as cass:
urlopen('http://www.iana.org/domains/reserved').read()
urlopen(httpbin.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):
assert last_mod == last_mod2
def test_disk_saver_write(tmpdir):
'''
@pytest.mark.online
def test_disk_saver_write(tmpdir, httpbin):
"""
Ensure that when you close a cassette after changing it it does
rewrite the file
'''
fname = str(tmpdir.join('synopsis.yaml'))
"""
fname = str(tmpdir.join("synopsis.yaml"))
with vcr.use_cassette(fname) as cass:
urlopen('http://www.iana.org/domains/reserved').read()
urlopen(httpbin.url).read()
assert cass.play_count == 0
last_mod = os.path.getmtime(fname)
@@ -45,9 +48,9 @@ def test_disk_saver_write(tmpdir):
# the mtime doesn't change
time.sleep(1)
with vcr.use_cassette(fname, record_mode='any') as cass:
urlopen('http://www.iana.org/domains/reserved').read()
urlopen('http://httpbin.org/').read()
with vcr.use_cassette(fname, record_mode=vcr.mode.ANY) as cass:
urlopen(httpbin.url).read()
urlopen(httpbin.url + "/get").read()
assert cass.play_count == 1
assert cass.dirty
last_mod2 = os.path.getmtime(fname)

View File

@@ -1,68 +1,171 @@
import base64
import json
from urllib.error import HTTPError
from urllib.parse import urlencode
from urllib.request import Request, urlopen
import pytest
from six.moves.urllib.request import urlopen, Request
from six.moves.urllib.error import HTTPError
import vcr
from ..assertions import assert_cassette_has_one_response, assert_is_json_bytes
def _request_with_auth(url, username, password):
request = Request(url)
base64string = base64.b64encode(
username.encode('ascii') + b':' + password.encode('ascii')
)
base64string = base64.b64encode(username.encode("ascii") + b":" + password.encode("ascii"))
request.add_header(b"Authorization", b"Basic " + base64string)
return urlopen(request)
def _find_header(cassette, header):
for request in cassette.requests:
for k in request.headers:
if header.lower() == k.lower():
return True
return False
return any(header in request.headers for request in cassette.requests)
def test_filter_basic_auth(tmpdir):
url = 'http://httpbin.org/basic-auth/user/passwd'
cass_file = str(tmpdir.join('basic_auth_filter.yaml'))
my_vcr = vcr.VCR(match_on=['uri', 'method', 'headers'])
def test_filter_basic_auth(tmpdir, httpbin):
url = httpbin.url + "/basic-auth/user/passwd"
cass_file = str(tmpdir.join("basic_auth_filter.yaml"))
my_vcr = vcr.VCR(match_on=["uri", "method", "headers"])
# 2 requests, one with auth failure and one with auth success
with my_vcr.use_cassette(cass_file, filter_headers=['authorization']):
with my_vcr.use_cassette(cass_file, filter_headers=["authorization"]):
with pytest.raises(HTTPError):
resp = _request_with_auth(url, 'user', 'wrongpasswd')
resp = _request_with_auth(url, "user", "wrongpasswd")
assert resp.getcode() == 401
resp = _request_with_auth(url, 'user', 'passwd')
resp = _request_with_auth(url, "user", "passwd")
assert resp.getcode() == 200
# make same 2 requests, this time both served from cassette.
with my_vcr.use_cassette(cass_file, filter_headers=['authorization']) as cass:
with my_vcr.use_cassette(cass_file, filter_headers=["authorization"]) as cass:
with pytest.raises(HTTPError):
resp = _request_with_auth(url, 'user', 'wrongpasswd')
resp = _request_with_auth(url, "user", "wrongpasswd")
assert resp.getcode() == 401
resp = _request_with_auth(url, 'user', 'passwd')
resp = _request_with_auth(url, "user", "passwd")
assert resp.getcode() == 200
# authorization header should not have been recorded
assert not _find_header(cass, 'authorization')
assert not _find_header(cass, "authorization")
assert len(cass) == 2
def test_filter_querystring(tmpdir):
url = 'http://httpbin.org/?foo=bar'
cass_file = str(tmpdir.join('filter_qs.yaml'))
with vcr.use_cassette(cass_file, filter_query_parameters=['foo']):
def test_filter_querystring(tmpdir, httpbin):
url = httpbin.url + "/?password=secret"
cass_file = str(tmpdir.join("filter_qs.yaml"))
with vcr.use_cassette(cass_file, filter_query_parameters=["password"]):
urlopen(url)
with vcr.use_cassette(cass_file, filter_query_parameters=['foo']) as cass:
with vcr.use_cassette(cass_file, filter_query_parameters=["password"]) as cass:
urlopen(url)
assert 'foo' not in cass.requests[0].url
assert "password" not in cass.requests[0].url
assert "secret" not in cass.requests[0].url
with open(cass_file) as f:
cassette_content = f.read()
assert "password" not in cassette_content
assert "secret" not in cassette_content
def test_filter_post_data(tmpdir, httpbin):
url = httpbin.url + "/post"
data = urlencode({"id": "secret", "foo": "bar"}).encode("utf-8")
cass_file = str(tmpdir.join("filter_pd.yaml"))
with vcr.use_cassette(cass_file, filter_post_data_parameters=["id"]):
urlopen(url, data)
with vcr.use_cassette(cass_file, filter_post_data_parameters=["id"]) as cass:
assert b"id=secret" not in cass.requests[0].body
def test_filter_json_post_data(tmpdir, httpbin):
data = json.dumps({"id": "secret", "foo": "bar"}).encode("utf-8")
request = Request(httpbin.url + "/post", data=data)
request.add_header("Content-Type", "application/json")
cass_file = str(tmpdir.join("filter_jpd.yaml"))
with vcr.use_cassette(cass_file, filter_post_data_parameters=["id"]):
urlopen(request)
with vcr.use_cassette(cass_file, filter_post_data_parameters=["id"]) as cass:
assert b'"id": "secret"' not in cass.requests[0].body
def test_filter_callback(tmpdir, httpbin):
url = httpbin.url + "/get"
cass_file = str(tmpdir.join("basic_auth_filter.yaml"))
def test_filter_callback(tmpdir):
url = 'http://httpbin.org/get'
cass_file = str(tmpdir.join('basic_auth_filter.yaml'))
def before_record_cb(request):
if request.path != '/get':
if request.path != "/get":
return request
my_vcr = vcr.VCR(
before_record = before_record_cb,
)
with my_vcr.use_cassette(cass_file, filter_headers=['authorization']) as cass:
# Test the legacy keyword.
my_vcr = vcr.VCR(before_record=before_record_cb)
with my_vcr.use_cassette(cass_file, filter_headers=["authorization"]) as cass:
urlopen(url)
assert len(cass) == 0
my_vcr = vcr.VCR(before_record_request=before_record_cb)
with my_vcr.use_cassette(cass_file, filter_headers=["authorization"]) as cass:
urlopen(url)
assert len(cass) == 0
def test_decompress_gzip(tmpdir, httpbin):
url = httpbin.url + "/gzip"
request = Request(url, headers={"Accept-Encoding": ["gzip, deflate"]})
cass_file = str(tmpdir.join("gzip_response.yaml"))
with vcr.use_cassette(cass_file, decode_compressed_response=True):
urlopen(request)
with vcr.use_cassette(cass_file) as cass:
decoded_response = urlopen(url).read()
assert_cassette_has_one_response(cass)
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):
url = httpbin.url + "/deflate"
request = Request(url, headers={"Accept-Encoding": ["gzip, deflate"]})
cass_file = str(tmpdir.join("deflate_response.yaml"))
with vcr.use_cassette(cass_file, decode_compressed_response=True):
urlopen(request)
with vcr.use_cassette(cass_file) as cass:
decoded_response = urlopen(url).read()
assert_cassette_has_one_response(cass)
assert_is_json_bytes(decoded_response)
def test_decompress_regular(tmpdir, httpbin):
"""Test that it doesn't try to decompress content that isn't compressed"""
url = httpbin.url + "/get"
cass_file = str(tmpdir.join("noncompressed_response.yaml"))
with vcr.use_cassette(cass_file, decode_compressed_response=True):
urlopen(url)
with vcr.use_cassette(cass_file) as cass:
resp = urlopen(url).read()
assert_cassette_has_one_response(cass)
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

@@ -0,0 +1,22 @@
interactions:
- request:
body: null
headers: {}
method: GET
uri: https://httpbin.org/get?ham=spam
response:
body: {string: "{\n \"args\": {\n \"ham\": \"spam\"\n }, \n \"headers\"\
: {\n \"Accept\": \"*/*\", \n \"Accept-Encoding\": \"gzip, deflate\"\
, \n \"Connection\": \"close\", \n \"Host\": \"httpbin.org\", \n \
\ \"User-Agent\": \"Python/3.5 aiohttp/2.0.1\"\n }, \n \"origin\": \"213.86.221.35\"\
, \n \"url\": \"https://httpbin.org/get?ham=spam\"\n}\n"}
headers: {Access-Control-Allow-Credentials: 'true', Access-Control-Allow-Origin: '*',
Connection: keep-alive, Content-Length: '299', Content-Type: application/json,
Date: 'Wed, 22 Mar 2017 20:08:29 GMT', Server: gunicorn/19.7.1, Via: 1.1 vegur}
status: {code: 200, message: OK}
url: !!python/object/new:yarl.URL
state: !!python/tuple
- !!python/object/new:urllib.parse.SplitResult [https, httpbin.org, /get, ham=spam,
'']
- false
version: 1

View File

@@ -1,140 +1,150 @@
'''Integration tests with httplib2'''
# coding=utf-8
"""Integration tests with httplib2"""
from urllib.parse import urlencode
# External imports
from six.moves.urllib_parse import urlencode
import pytest
import pytest_httpbin.certs
# Internal imports
import vcr
from assertions import assert_cassette_has_one_response
from ..assertions import assert_cassette_has_one_response
httplib2 = pytest.importorskip("httplib2")
@pytest.fixture(params=["https", "http"])
def scheme(request):
def http():
"""
Fixture that returns both http and https
Returns an httplib2 HTTP instance
with the certificate replaced by the httpbin one.
"""
return request.param
kwargs = {"ca_certs": pytest_httpbin.certs.where()}
return httplib2.Http(**kwargs)
def test_response_code(scheme, tmpdir):
'''Ensure we can read a response code from a fetch'''
url = scheme + '://httpbin.org/'
with vcr.use_cassette(str(tmpdir.join('atts.yaml'))) as cass:
resp, _ = httplib2.Http().request(url)
def test_response_code(tmpdir, httpbin_both):
"""Ensure we can read a response code from a fetch"""
url = httpbin_both.url
with vcr.use_cassette(str(tmpdir.join("atts.yaml"))):
resp, _ = http().request(url)
code = resp.status
with vcr.use_cassette(str(tmpdir.join('atts.yaml'))) as cass:
resp, _ = httplib2.Http().request(url)
with vcr.use_cassette(str(tmpdir.join("atts.yaml"))):
resp, _ = http().request(url)
assert code == resp.status
def test_random_body(scheme, tmpdir):
'''Ensure we can read the content, and that it's served from cache'''
url = scheme + '://httpbin.org/bytes/1024'
with vcr.use_cassette(str(tmpdir.join('body.yaml'))) as cass:
_, content = httplib2.Http().request(url)
def test_random_body(httpbin_both, tmpdir):
"""Ensure we can read the content, and that it's served from cache"""
url = httpbin_both.url + "/bytes/1024"
with vcr.use_cassette(str(tmpdir.join("body.yaml"))):
_, content = http().request(url)
body = content
with vcr.use_cassette(str(tmpdir.join('body.yaml'))) as cass:
_, content = httplib2.Http().request(url)
with vcr.use_cassette(str(tmpdir.join("body.yaml"))):
_, content = http().request(url)
assert body == content
def test_response_headers(scheme, tmpdir):
'''Ensure we can get information from the response'''
url = scheme + '://httpbin.org/'
with vcr.use_cassette(str(tmpdir.join('headers.yaml'))) as cass:
resp, _ = httplib2.Http().request(url)
def test_response_headers(tmpdir, httpbin_both):
"""Ensure we can get information from the response"""
url = httpbin_both.url
with vcr.use_cassette(str(tmpdir.join("headers.yaml"))):
resp, _ = http().request(url)
headers = resp.items()
with vcr.use_cassette(str(tmpdir.join('headers.yaml'))) as cass:
resp, _ = httplib2.Http().request(url)
assert headers == resp.items()
with vcr.use_cassette(str(tmpdir.join("headers.yaml"))):
resp, _ = http().request(url)
assert set(headers) == set(resp.items())
def test_multiple_requests(scheme, tmpdir):
'''Ensure that we can cache multiple requests'''
urls = [
scheme + '://httpbin.org/',
scheme + '://httpbin.org/',
scheme + '://httpbin.org/get',
scheme + '://httpbin.org/bytes/1024'
]
with vcr.use_cassette(str(tmpdir.join('multiple.yaml'))) as cass:
[httplib2.Http().request(url) for url in urls]
@pytest.mark.online
def test_effective_url(tmpdir, httpbin):
"""Ensure that the effective_url is captured"""
url = httpbin.url + "/redirect-to?url=.%2F&status_code=301"
with vcr.use_cassette(str(tmpdir.join("headers.yaml"))):
resp, _ = http().request(url)
effective_url = resp["content-location"]
assert effective_url == httpbin.url + "/"
with vcr.use_cassette(str(tmpdir.join("headers.yaml"))):
resp, _ = http().request(url)
assert effective_url == resp["content-location"]
def test_multiple_requests(tmpdir, httpbin_both):
"""Ensure that we can cache multiple requests"""
urls = [httpbin_both.url, httpbin_both.url, httpbin_both.url + "/get", httpbin_both.url + "/bytes/1024"]
with vcr.use_cassette(str(tmpdir.join("multiple.yaml"))) as cass:
[http().request(url) for url in urls]
assert len(cass) == len(urls)
def test_get_data(scheme, tmpdir):
'''Ensure that it works with query data'''
data = urlencode({'some': 1, 'data': 'here'})
url = scheme + '://httpbin.org/get?' + data
with vcr.use_cassette(str(tmpdir.join('get_data.yaml'))) as cass:
_, res1 = httplib2.Http().request(url)
def test_get_data(tmpdir, httpbin_both):
"""Ensure that it works with query data"""
data = urlencode({"some": 1, "data": "here"})
url = httpbin_both.url + "/get?" + data
with vcr.use_cassette(str(tmpdir.join("get_data.yaml"))):
_, res1 = http().request(url)
with vcr.use_cassette(str(tmpdir.join('get_data.yaml'))) as cass:
_, res2 = httplib2.Http().request(url)
with vcr.use_cassette(str(tmpdir.join("get_data.yaml"))):
_, res2 = http().request(url)
assert res1 == res2
def test_post_data(scheme, tmpdir):
'''Ensure that it works when posting data'''
data = urlencode({'some': 1, 'data': 'here'})
url = scheme + '://httpbin.org/post'
with vcr.use_cassette(str(tmpdir.join('post_data.yaml'))) as cass:
_, res1 = httplib2.Http().request(url, "POST", data)
def test_post_data(tmpdir, httpbin_both):
"""Ensure that it works when posting data"""
data = urlencode({"some": 1, "data": "here"})
url = httpbin_both.url + "/post"
with vcr.use_cassette(str(tmpdir.join("post_data.yaml"))):
_, res1 = http().request(url, "POST", data)
with vcr.use_cassette(str(tmpdir.join('post_data.yaml'))) as cass:
_, res2 = httplib2.Http().request(url, "POST", data)
with vcr.use_cassette(str(tmpdir.join("post_data.yaml"))) as cass:
_, res2 = http().request(url, "POST", data)
assert res1 == res2
assert_cassette_has_one_response(cass)
def test_post_unicode_data(scheme, tmpdir):
'''Ensure that it works when posting unicode data'''
data = urlencode({'snowman': u''.encode('utf-8')})
url = scheme + '://httpbin.org/post'
with vcr.use_cassette(str(tmpdir.join('post_data.yaml'))) as cass:
_, res1 = httplib2.Http().request(url, "POST", data)
def test_post_unicode_data(tmpdir, httpbin_both):
"""Ensure that it works when posting unicode data"""
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)
with vcr.use_cassette(str(tmpdir.join('post_data.yaml'))) as cass:
_, res2 = httplib2.Http().request(url, "POST", data)
with vcr.use_cassette(str(tmpdir.join("post_data.yaml"))) as cass:
_, res2 = http().request(url, "POST", data)
assert res1 == res2
assert_cassette_has_one_response(cass)
def test_cross_scheme(tmpdir):
'''Ensure that requests between schemes are treated separately'''
def test_cross_scheme(tmpdir, httpbin, httpbin_secure):
"""Ensure that requests between schemes are treated separately"""
# First fetch a url under https, and then again under https and then
# ensure that we haven't served anything out of cache, and we have two
# requests / response pairs in the cassette
with vcr.use_cassette(str(tmpdir.join('cross_scheme.yaml'))) as cass:
httplib2.Http().request('https://httpbin.org/')
httplib2.Http().request('http://httpbin.org/')
with vcr.use_cassette(str(tmpdir.join("cross_scheme.yaml"))) as cass:
http().request(httpbin_secure.url)
http().request(httpbin.url)
assert len(cass) == 2
assert cass.play_count == 0
def test_decorator(scheme, tmpdir):
'''Test the decorator version of VCR.py'''
url = scheme + '://httpbin.org/'
def test_decorator(tmpdir, httpbin_both):
"""Test the decorator version of VCR.py"""
url = httpbin_both.url
@vcr.use_cassette(str(tmpdir.join('atts.yaml')))
@vcr.use_cassette(str(tmpdir.join("atts.yaml")))
def inner1():
resp, _ = httplib2.Http().request(url)
return resp['status']
resp, _ = http().request(url)
return resp["status"]
@vcr.use_cassette(str(tmpdir.join('atts.yaml')))
@vcr.use_cassette(str(tmpdir.join("atts.yaml")))
def inner2():
resp, _ = httplib2.Http().request(url)
return resp['status']
resp, _ = http().request(url)
return resp["status"]
assert inner1() == inner2()

View File

@@ -0,0 +1,362 @@
import os
import pytest
import vcr
from ..assertions import assert_is_json_bytes
asyncio = pytest.importorskip("asyncio")
httpx = pytest.importorskip("httpx")
@pytest.fixture(params=["https", "http"])
def scheme(request):
"""Fixture that returns both http and https."""
return request.param
class BaseDoRequest:
_client_class = None
def __init__(self, *args, **kwargs):
self._client_args = args
self._client_kwargs = kwargs
self._client_kwargs["follow_redirects"] = self._client_kwargs.get("follow_redirects", True)
def _make_client(self):
return self._client_class(*self._client_args, **self._client_kwargs)
class DoSyncRequest(BaseDoRequest):
_client_class = httpx.Client
def __enter__(self):
self._client = self._make_client()
return self
def __exit__(self, *args):
self._client.close()
del self._client
@property
def client(self):
try:
return self._client
except AttributeError as e:
raise ValueError('To access sync client, use "with do_request() as client"') from e
def __call__(self, *args, **kwargs):
if hasattr(self, "_client"):
return self.client.request(*args, timeout=60, **kwargs)
# Use one-time context and dispose of the client afterwards
with self:
return self.client.request(*args, timeout=60, **kwargs)
def stream(self, *args, **kwargs):
if hasattr(self, "_client"):
with self.client.stream(*args, **kwargs) as response:
return b"".join(response.iter_bytes())
# Use one-time context and dispose of the client afterwards
with self, self.client.stream(*args, **kwargs) as response:
return b"".join(response.iter_bytes())
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 as e:
raise ValueError('To access async client, use "with do_request() as client"') from e
def __call__(self, *args, **kwargs):
if hasattr(self, "_loop"):
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._loop.run_until_complete(self.client.request(*args, **kwargs))
async def _get_stream(self, *args, **kwargs):
async with self.client.stream(*args, **kwargs) as response:
content = b""
async for c in response.aiter_bytes():
content += c
return content
def stream(self, *args, **kwargs):
if hasattr(self, "_loop"):
return self._loop.run_until_complete(self._get_stream(*args, **kwargs))
# Use one-time context and dispose of the loop/client afterwards
with self:
return self._loop.run_until_complete(self._get_stream(*args, **kwargs))
def pytest_generate_tests(metafunc):
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, httpbin, do_request):
url = httpbin.url
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, httpbin, do_request):
url = httpbin.url
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, httpbin, do_request):
url = httpbin.url
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, httpbin, do_request):
url = httpbin.url + "/json"
with vcr.use_cassette(str(tmpdir.join("json.yaml"))):
response = do_request()("GET", url)
with vcr.use_cassette(str(tmpdir.join("json.yaml"))) as cassette:
cassette_response = do_request()("GET", url)
assert cassette_response.json() == response.json()
assert cassette.play_count == 1
@pytest.mark.online
def test_params_same_url_distinct_params(tmpdir, httpbin, do_request):
url = httpbin.url + "/get"
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,
pytest.raises(vcr.errors.CannotOverwriteExistingCassetteException),
):
do_request()("GET", url, params=params, headers=headers)
@pytest.mark.online
def test_redirect(httpbin, yml, do_request):
url = httpbin.url + "/redirect-to"
response = do_request()("GET", url)
with vcr.use_cassette(yml):
response = do_request()("GET", url, params={"url": "./get", "status_code": 302})
with vcr.use_cassette(yml) as cassette:
cassette_response = do_request()("GET", url, params={"url": "./get", "status_code": 302})
assert cassette_response.status_code == response.status_code
assert len(cassette_response.history) == len(response.history)
assert len(cassette) == 2
assert cassette.play_count == 2
# Assert that the real response and the cassette response have a similar
# looking request_info.
assert cassette_response.request.url == response.request.url
assert cassette_response.request.method == response.request.method
assert cassette_response.request.headers.items() == response.request.headers.items()
@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
@pytest.mark.online
def test_cookies(tmpdir, httpbin, do_request):
def client_cookies(client):
return list(client.client.cookies)
def response_cookies(response):
return list(response.cookies)
url = httpbin.url + "/cookies/set"
params = {"k1": "v1", "k2": "v2"}
with do_request(params=params, follow_redirects=False) as client:
assert client_cookies(client) == []
testfile = str(tmpdir.join("cookies.yml"))
with vcr.use_cassette(testfile):
r1 = client("GET", url)
assert response_cookies(r1) == ["k1", "k2"]
r2 = client("GET", url)
assert response_cookies(r2) == ["k1", "k2"]
assert client_cookies(client) == ["k1", "k2"]
with do_request(params=params, follow_redirects=False) 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_stream(tmpdir, httpbin, do_request):
url = httpbin.url + "/stream-bytes/512"
testfile = str(tmpdir.join("stream.yml"))
with vcr.use_cassette(testfile):
response_content = do_request().stream("GET", url)
assert len(response_content) == 512
with vcr.use_cassette(testfile) as cassette:
cassette_content = do_request().stream("GET", url)
assert cassette_content == response_content
assert len(cassette_content) == 512
assert cassette.play_count == 1
# Regular cassette formats support the status reason,
# but the old HTTPX cassette format does not.
@pytest.mark.parametrize(
"cassette_name,reason",
[
("requests", "great"),
("httpx_old_format", "OK"),
],
)
def test_load_cassette_format(do_request, cassette_name, reason):
mydir = os.path.dirname(os.path.realpath(__file__))
yml = f"{mydir}/cassettes/gzip_{cassette_name}.yaml"
url = "https://httpbin.org/gzip"
with vcr.use_cassette(yml) as cassette:
cassette_response = do_request()("GET", url)
assert str(cassette_response.request.url) == url
assert cassette.play_count == 1
# Should be able to load up the JSON inside,
# regardless whether the content is the gzipped
# in the cassette or not.
json = cassette_response.json()
assert json["method"] == "GET", json
assert cassette_response.status_code == 200
assert cassette_response.reason_phrase == reason
def test_gzip__decode_compressed_response_false(tmpdir, httpbin, do_request):
"""
Ensure that httpx is able to automatically decompress the response body.
"""
for _ in range(2): # one for recording, one for re-playing
with vcr.use_cassette(str(tmpdir.join("gzip.yaml"))) as cassette:
response = do_request()("GET", httpbin + "/gzip")
assert response.headers["content-encoding"] == "gzip" # i.e. not removed
# The content stored in the cassette should be gzipped.
assert cassette.responses[0]["body"]["string"][:2] == b"\x1f\x8b"
assert_is_json_bytes(response.content) # i.e. uncompressed bytes
def test_gzip__decode_compressed_response_true(do_request, tmpdir, httpbin):
url = httpbin + "/gzip"
expected_response = do_request()("GET", url)
expected_content = expected_response.content
assert expected_response.headers["content-encoding"] == "gzip" # self-test
with vcr.use_cassette(
str(tmpdir.join("decode_compressed.yaml")),
decode_compressed_response=True,
) as cassette:
r = do_request()("GET", url)
assert r.headers["content-encoding"] == "gzip" # i.e. not removed
content_length = r.headers["content-length"]
assert r.content == expected_content
# Has the cassette body been decompressed?
cassette_response_body = cassette.responses[0]["body"]["string"]
assert isinstance(cassette_response_body, str)
# Content should be JSON.
assert cassette_response_body[0:1] == "{"
with vcr.use_cassette(str(tmpdir.join("decode_compressed.yaml")), decode_compressed_response=True):
r = httpx.get(url)
assert "content-encoding" not in r.headers # i.e. removed
assert r.content == expected_content
# As the content is uncompressed, it should have a bigger
# length than the compressed version.
assert r.headers["content-length"] > content_length

View File

@@ -1,55 +1,68 @@
import base64
import pytest
from six.moves.urllib.request import urlopen, Request
from six.moves.urllib.error import HTTPError
import socket
from contextlib import contextmanager
from urllib.request import urlopen
import vcr
def test_ignore_localhost(tmpdir, httpserver):
httpserver.serve_content('Hello!')
cass_file = str(tmpdir.join('filter_qs.yaml'))
with vcr.use_cassette(cass_file, ignore_localhost=True) as cass:
urlopen(httpserver.url)
assert len(cass) == 0
urlopen('http://httpbin.org')
assert len(cass) == 1
@contextmanager
def overridden_dns(overrides):
"""
Monkeypatch socket.getaddrinfo() to override DNS lookups (name will resolve
to address)
"""
real_getaddrinfo = socket.getaddrinfo
def fake_getaddrinfo(*args, **kwargs):
if args[0] in overrides:
address = overrides[args[0]]
return [(2, 1, 6, "", (address, args[1]))]
return real_getaddrinfo(*args, **kwargs)
socket.getaddrinfo = fake_getaddrinfo
yield
socket.getaddrinfo = real_getaddrinfo
def test_ignore_httpbin(tmpdir, httpserver):
httpserver.serve_content('Hello!')
cass_file = str(tmpdir.join('filter_qs.yaml'))
with vcr.use_cassette(
cass_file,
ignore_hosts=['httpbin.org']
) as cass:
urlopen('http://httpbin.org')
assert len(cass) == 0
urlopen(httpserver.url)
assert len(cass) == 1
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(f"http://localhost:{httpbin.port}/")
assert len(cass) == 0
urlopen(f"http://httpbin.org:{httpbin.port}/")
assert len(cass) == 1
def test_ignore_localhost_and_httpbin(tmpdir, httpserver):
httpserver.serve_content('Hello!')
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')
urlopen(httpserver.url)
assert len(cass) == 0
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(f"http://httpbin.org:{httpbin.port}/")
assert len(cass) == 0
urlopen(f"http://localhost:{httpbin.port}/")
assert len(cass) == 1
def test_ignore_localhost_twice(tmpdir, httpserver):
httpserver.serve_content('Hello!')
cass_file = str(tmpdir.join('filter_qs.yaml'))
with vcr.use_cassette(cass_file, ignore_localhost=True) as cass:
urlopen(httpserver.url)
assert len(cass) == 0
urlopen('http://httpbin.org')
assert len(cass) == 1
with vcr.use_cassette(cass_file, ignore_localhost=True) as cass:
assert len(cass) == 1
urlopen(httpserver.url)
urlopen('http://httpbin.org')
assert len(cass) == 1
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(f"http://httpbin.org:{httpbin.port}")
urlopen(f"http://localhost:{httpbin.port}")
assert len(cass) == 0
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(f"http://localhost:{httpbin.port}")
assert len(cass) == 0
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(f"http://localhost:{httpbin.port}")
urlopen(f"http://httpbin.org:{httpbin.port}")
assert len(cass) == 1

View File

@@ -1,47 +1,48 @@
import vcr
from urllib.request import urlopen
import pytest
from six.moves.urllib.request import urlopen
import vcr
DEFAULT_URI = "http://httpbin.org/get?p1=q1&p2=q2" # base uri for testing
DEFAULT_URI = 'http://httpbin.org/get?p1=q1&p2=q2' # base uri for testing
def _replace_httpbin(uri, httpbin, httpbin_secure):
return uri.replace("http://httpbin.org", httpbin.url).replace("https://httpbin.org", httpbin_secure.url)
@pytest.fixture
def cassette(tmpdir):
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
"""
cassette_path = str(tmpdir.join('test.yml'))
with vcr.use_cassette(cassette_path, record_mode='all'):
urlopen(DEFAULT_URI)
default_uri = _replace_httpbin(DEFAULT_URI, httpbin, httpbin_secure)
cassette_path = str(tmpdir.join("test.yml"))
with vcr.use_cassette(cassette_path, record_mode=vcr.mode.ALL):
urlopen(default_uri)
return cassette_path
@pytest.mark.parametrize("matcher, matching_uri, not_matching_uri", [
('uri',
'http://httpbin.org/get?p1=q1&p2=q2',
'http://httpbin.org/get?p2=q2&p1=q1'),
('scheme',
'http://google.com/post?a=b',
'https://httpbin.org/get?p1=q1&p2=q2'),
('host',
'https://httpbin.org/post?a=b',
'http://google.com/get?p1=q1&p2=q2'),
('port',
'https://google.com:80/post?a=b',
'http://httpbin.org:5000/get?p1=q1&p2=q2'),
('path',
'https://google.com/get?a=b',
'http://httpbin.org/post?p1=q1&p2=q2'),
('query',
'https://google.com/get?p2=q2&p1=q1',
'http://httpbin.org/get?p1=q1&a=b')
])
def test_matchers(cassette, matcher, matching_uri, not_matching_uri):
@pytest.mark.parametrize(
"matcher, matching_uri, not_matching_uri",
[
("uri", "http://httpbin.org/get?p1=q1&p2=q2", "http://httpbin.org/get?p2=q2&p1=q1"),
("scheme", "http://google.com/post?a=b", "https://httpbin.org/get?p1=q1&p2=q2"),
("host", "https://httpbin.org/post?a=b", "http://google.com/get?p1=q1&p2=q2"),
("path", "https://google.com/get?a=b", "http://httpbin.org/post?p1=q1&p2=q2"),
("query", "https://google.com/get?p2=q2&p1=q1", "http://httpbin.org/get?p1=q1&a=b"),
],
)
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)
# play cassette with default uri
with vcr.use_cassette(cassette, match_on=[matcher]) as cass:
urlopen(DEFAULT_URI)
urlopen(default_uri)
assert cass.play_count == 1
# play cassette with matching on uri
@@ -50,50 +51,63 @@ def test_matchers(cassette, matcher, matching_uri, not_matching_uri):
assert cass.play_count == 1
# play cassette with not matching on uri, it should fail
with pytest.raises(vcr.errors.CannotOverwriteExistingCassetteException):
with vcr.use_cassette(cassette, match_on=[matcher]) as cass:
urlopen(not_matching_uri)
with (
pytest.raises(vcr.errors.CannotOverwriteExistingCassetteException),
vcr.use_cassette(cassette, match_on=[matcher]) as cass,
):
urlopen(not_matching_uri)
def test_method_matcher(cassette):
def test_method_matcher(cassette, httpbin, httpbin_secure):
default_uri = _replace_httpbin(DEFAULT_URI, httpbin, httpbin_secure)
# play cassette with matching on method
with vcr.use_cassette(cassette, match_on=['method']) as cass:
urlopen('https://google.com/get?a=b')
with vcr.use_cassette(cassette, match_on=["method"]) as cass:
urlopen("https://google.com/get?a=b")
assert cass.play_count == 1
# should fail if method does not match
with pytest.raises(vcr.errors.CannotOverwriteExistingCassetteException):
with vcr.use_cassette(cassette, match_on=['method']) as cass:
# is a POST request
urlopen(DEFAULT_URI, data=b'')
with (
pytest.raises(vcr.errors.CannotOverwriteExistingCassetteException),
vcr.use_cassette(cassette, match_on=["method"]) as cass,
):
# is a POST request
urlopen(default_uri, data=b"")
@pytest.mark.parametrize("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):
@pytest.mark.parametrize(
"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:
urlopen(uri)
assert cass.play_count == 1
@pytest.mark.parametrize("uri", [
'https://httpbin.org/get?p1=q1&p2=q2',
'http://google.com/get?p1=q1&p2=q2',
'http://httpbin.org:5000/get?p1=q1&p2=q2',
'http://httpbin.org/post?p1=q1&p2=q2',
'http://httpbin.org/get?p1=q1&a=b'
])
def test_default_matcher_does_not_match(cassette, uri):
with pytest.raises(vcr.errors.CannotOverwriteExistingCassetteException):
with vcr.use_cassette(cassette):
urlopen(uri)
@pytest.mark.parametrize(
"uri",
[
"https://httpbin.org/get?p1=q1&p2=q2",
"http://google.com/get?p1=q1&p2=q2",
"http://httpbin.org/post?p1=q1&p2=q2",
"http://httpbin.org/get?p1=q1&a=b",
],
)
def test_default_matcher_does_not_match(cassette, uri, httpbin, httpbin_secure):
uri = _replace_httpbin(uri, httpbin, httpbin_secure)
with pytest.raises(vcr.errors.CannotOverwriteExistingCassetteException), vcr.use_cassette(cassette):
urlopen(uri)
def test_default_matcher_does_not_match_on_method(cassette):
with pytest.raises(vcr.errors.CannotOverwriteExistingCassetteException):
with vcr.use_cassette(cassette):
# is a POST request
urlopen(DEFAULT_URI, data=b'')
def test_default_matcher_does_not_match_on_method(cassette, httpbin, httpbin_secure):
default_uri = _replace_httpbin(DEFAULT_URI, httpbin, httpbin_secure)
with pytest.raises(vcr.errors.CannotOverwriteExistingCassetteException), vcr.use_cassette(cassette):
# is a POST request
urlopen(default_uri, data=b"")

View File

@@ -1,20 +1,23 @@
from urllib.request import urlopen
import pytest
import vcr
from six.moves.urllib.request import urlopen
from vcr.errors import CannotOverwriteExistingCassetteException
def test_making_extra_request_raises_exception(tmpdir):
def test_making_extra_request_raises_exception(tmpdir, httpbin):
# make two requests in the first request that are considered
# identical (since the match is based on method)
with vcr.use_cassette(str(tmpdir.join('test.json')), match_on=['method']):
urlopen('http://httpbin.org/status/200')
urlopen('http://httpbin.org/status/201')
with vcr.use_cassette(str(tmpdir.join("test.json")), match_on=["method"]):
urlopen(httpbin.url + "/status/200")
urlopen(httpbin.url + "/status/201")
# Now, try to make three requests. The first two should return the
# correct status codes in order, and the third should raise an
# exception.
with vcr.use_cassette(str(tmpdir.join('test.json')), match_on=['method']):
assert urlopen('http://httpbin.org/status/200').getcode() == 200
assert urlopen('http://httpbin.org/status/201').getcode() == 201
with pytest.raises(Exception):
urlopen('http://httpbin.org/status/200')
with vcr.use_cassette(str(tmpdir.join("test.json")), match_on=["method"]):
assert urlopen(httpbin.url + "/status/200").getcode() == 200
assert urlopen(httpbin.url + "/status/201").getcode() == 201
with pytest.raises(CannotOverwriteExistingCassetteException):
urlopen(httpbin.url + "/status/200")

View File

@@ -0,0 +1,107 @@
"""Test using a proxy."""
import asyncio
import http.server
import socketserver
import threading
from urllib.request import urlopen
import pytest
import vcr
# Conditional imports
requests = pytest.importorskip("requests")
class Proxy(http.server.SimpleHTTPRequestHandler):
"""
Simple proxy server.
(Inspired by: http://effbot.org/librarybook/simplehttpserver.htm).
"""
def do_GET(self):
upstream_response = urlopen(self.path)
try:
status = upstream_response.status
headers = upstream_response.headers.items()
except AttributeError:
# In Python 2 the response is an addinfourl instance.
status = upstream_response.code
headers = upstream_response.info().items()
self.log_request(status)
self.send_response_only(status, upstream_response.msg)
for header in headers:
self.send_header(*header)
self.end_headers()
self.copyfile(upstream_response, self.wfile)
def do_CONNECT(self):
host, port = self.path.split(":")
asyncio.run(self._tunnel(host, port, self.connection))
async def _tunnel(self, host, port, client_sock):
target_r, target_w = await asyncio.open_connection(host=host, port=port)
self.send_response(http.HTTPStatus.OK)
self.end_headers()
source_r, source_w = await asyncio.open_connection(sock=client_sock)
async def channel(reader, writer):
while True:
data = await reader.read(1024)
if not data:
break
writer.write(data)
await writer.drain()
writer.close()
await writer.wait_closed()
await asyncio.gather(
channel(target_r, source_w),
channel(source_r, target_w),
)
@pytest.fixture(scope="session")
def proxy_server():
with socketserver.ThreadingTCPServer(("", 0), Proxy) as httpd:
proxy_process = threading.Thread(target=httpd.serve_forever)
proxy_process.start()
yield "http://{}:{}".format(*httpd.server_address)
httpd.shutdown()
proxy_process.join()
def test_use_proxy(tmpdir, httpbin, proxy_server):
"""Ensure that it works with a proxy."""
with vcr.use_cassette(str(tmpdir.join("proxy.yaml"))):
response = requests.get(httpbin.url, proxies={"http": proxy_server})
with vcr.use_cassette(str(tmpdir.join("proxy.yaml")), mode="none") as cassette:
cassette_response = requests.get(httpbin.url, proxies={"http": proxy_server})
assert cassette_response.headers == response.headers
assert cassette.play_count == 1
def test_use_https_proxy(tmpdir, httpbin_secure, proxy_server):
"""Ensure that it works with an HTTPS proxy."""
with vcr.use_cassette(str(tmpdir.join("proxy.yaml"))):
response = requests.get(httpbin_secure.url, proxies={"https": proxy_server})
with vcr.use_cassette(str(tmpdir.join("proxy.yaml")), mode="none") as cassette:
cassette_response = requests.get(
httpbin_secure.url,
proxies={"https": proxy_server},
)
assert cassette_response.headers == response.headers
assert cassette.play_count == 1
# The cassette URL points to httpbin, not the proxy
assert cassette.requests[0].url == httpbin_secure.url + "/"

View File

@@ -1,65 +1,67 @@
import os
from urllib.request import urlopen
import pytest
import vcr
from six.moves.urllib.request import urlopen
from vcr.errors import CannotOverwriteExistingCassetteException
def test_once_record_mode(tmpdir):
testfile = str(tmpdir.join('recordmode.yml'))
with vcr.use_cassette(testfile, record_mode="once"):
def test_once_record_mode(tmpdir, httpbin):
testfile = str(tmpdir.join("recordmode.yml"))
with vcr.use_cassette(testfile, record_mode=vcr.mode.ONCE):
# cassette file doesn't exist, so create.
response = urlopen('http://httpbin.org/').read()
urlopen(httpbin.url).read()
with vcr.use_cassette(testfile, record_mode="once") as cass:
with vcr.use_cassette(testfile, record_mode=vcr.mode.ONCE):
# make the same request again
response = urlopen('http://httpbin.org/').read()
urlopen(httpbin.url).read()
# the first time, it's played from the cassette.
# but, try to access something else from the same cassette, and an
# exception is raised.
with pytest.raises(Exception):
response = urlopen('http://httpbin.org/get').read()
with pytest.raises(CannotOverwriteExistingCassetteException):
urlopen(httpbin.url + "/get").read()
def test_once_record_mode_two_times(tmpdir):
testfile = str(tmpdir.join('recordmode.yml'))
with vcr.use_cassette(testfile, record_mode="once"):
def test_once_record_mode_two_times(tmpdir, httpbin):
testfile = str(tmpdir.join("recordmode.yml"))
with vcr.use_cassette(testfile, record_mode=vcr.mode.ONCE):
# get two of the same file
response1 = urlopen('http://httpbin.org/').read()
response2 = urlopen('http://httpbin.org/').read()
urlopen(httpbin.url).read()
urlopen(httpbin.url).read()
with vcr.use_cassette(testfile, record_mode="once") as cass:
with vcr.use_cassette(testfile, record_mode=vcr.mode.ONCE):
# do it again
response = urlopen('http://httpbin.org/').read()
response = urlopen('http://httpbin.org/').read()
urlopen(httpbin.url).read()
urlopen(httpbin.url).read()
def test_once_mode_three_times(tmpdir):
testfile = str(tmpdir.join('recordmode.yml'))
with vcr.use_cassette(testfile, record_mode="once"):
def test_once_mode_three_times(tmpdir, httpbin):
testfile = str(tmpdir.join("recordmode.yml"))
with vcr.use_cassette(testfile, record_mode=vcr.mode.ONCE):
# get three of the same file
response1 = urlopen('http://httpbin.org/').read()
response2 = urlopen('http://httpbin.org/').read()
response2 = urlopen('http://httpbin.org/').read()
urlopen(httpbin.url).read()
urlopen(httpbin.url).read()
urlopen(httpbin.url).read()
def test_new_episodes_record_mode(tmpdir):
testfile = str(tmpdir.join('recordmode.yml'))
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.
response = urlopen('http://httpbin.org/').read()
urlopen(httpbin.url).read()
with vcr.use_cassette(testfile, record_mode="new_episodes") as cass:
with vcr.use_cassette(testfile, record_mode=vcr.mode.NEW_EPISODES) as cass:
# make the same request again
response = urlopen('http://httpbin.org/').read()
urlopen(httpbin.url).read()
# all responses have been played
assert cass.all_played
# in the "new_episodes" record mode, we can add more requests to
# a cassette without repurcussions.
response = urlopen('http://httpbin.org/get').read()
# a cassette without repercussions.
urlopen(httpbin.url + "/get").read()
# one of the responses has been played
assert cass.play_count == 1
@@ -67,25 +69,50 @@ def test_new_episodes_record_mode(tmpdir):
# 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
def test_all_record_mode(tmpdir):
testfile = str(tmpdir.join('recordmode.yml'))
with vcr.use_cassette(testfile, record_mode="all"):
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=vcr.mode.NEW_EPISODES):
# cassette file doesn't exist, so create.
response = urlopen('http://httpbin.org/').read()
original_first_response = urlopen(url).read()
with vcr.use_cassette(testfile, record_mode="all") as cass:
with vcr.use_cassette(testfile, record_mode=vcr.mode.NEW_EPISODES):
# make the same request again
response = urlopen('http://httpbin.org/').read()
assert urlopen(url).read() == original_first_response
# in the "new_episodes" record mode, we can add the same request
# to the cassette without repercussions
original_second_response = urlopen(url).read()
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
# now that we are back in once mode, this should raise
# an error.
with pytest.raises(CannotOverwriteExistingCassetteException):
urlopen(url).read()
def test_all_record_mode(tmpdir, httpbin):
testfile = str(tmpdir.join("recordmode.yml"))
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=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.
response = urlopen('http://httpbin.org/get').read()
# a cassette without repercussions.
urlopen(httpbin.url + "/get").read()
# The cassette was never actually played, even though it existed.
# that's because, in "all" mode, the requests all go directly to
@@ -93,26 +120,28 @@ def test_all_record_mode(tmpdir):
assert cass.play_count == 0
def test_none_record_mode(tmpdir):
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 pytest.raises(Exception):
response = urlopen('http://httpbin.org/').read()
testfile = str(tmpdir.join("recordmode.yml"))
with (
vcr.use_cassette(testfile, record_mode=vcr.mode.NONE),
pytest.raises(CannotOverwriteExistingCassetteException),
):
urlopen(httpbin.url).read()
def test_none_record_mode_with_existing_cassette(tmpdir):
def test_none_record_mode_with_existing_cassette(tmpdir, httpbin):
# create a cassette file
testfile = str(tmpdir.join('recordmode.yml'))
testfile = str(tmpdir.join("recordmode.yml"))
with vcr.use_cassette(testfile, record_mode="all"):
response = urlopen('http://httpbin.org/').read()
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:
response = urlopen('http://httpbin.org/').read()
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.
with pytest.raises(Exception):
response = urlopen('http://httpbin.org/get').read()
with pytest.raises(CannotOverwriteExistingCassetteException):
urlopen(httpbin.url + "/get").read()

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):
@pytest.mark.online
def test_registered_true_matcher(tmpdir, httpbin):
my_vcr = vcr.VCR()
my_vcr.register_matcher('true', true_matcher)
testfile = str(tmpdir.join('test.yml'))
with my_vcr.use_cassette(testfile, match_on=['true']) as cass:
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('http://httpbin.org/')
urlopen('https://httpbin.org/get')
urlopen(httpbin.url)
urlopen(httpbin.url + "/get")
with my_vcr.use_cassette(testfile, match_on=['true']) as cass:
with my_vcr.use_cassette(testfile, match_on=["true"]):
# I can get the response twice even though I only asked for it once
urlopen('http://httpbin.org/get')
urlopen('https://httpbin.org/get')
urlopen(httpbin.url)
urlopen(httpbin.url)
def test_registered_false_matcher(tmpdir):
@pytest.mark.online
def test_registered_false_matcher(tmpdir, httpbin):
my_vcr = vcr.VCR()
my_vcr.register_matcher('false', false_matcher)
testfile = str(tmpdir.join('test.yml'))
with my_vcr.use_cassette(testfile, match_on=['false']) as cass:
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('http://httpbin.org/')
urlopen('https://httpbin.org/get')
urlopen(httpbin.url)
urlopen(httpbin.url + "/get")
assert len(cass) == 2

View File

@@ -0,0 +1,87 @@
"""Tests for cassettes with custom persistence"""
# External imports
import os
from urllib.request import urlopen
import pytest
# Internal imports
import vcr
from vcr.persisters.filesystem import CassetteDecodeError, CassetteNotFoundError, FilesystemPersister
class CustomFilesystemPersister:
"""Behaves just like default FilesystemPersister but adds .test extension
to the cassette file"""
@staticmethod
def load_cassette(cassette_path, serializer):
cassette_path += ".test"
return FilesystemPersister.load_cassette(cassette_path, serializer)
@staticmethod
def save_cassette(cassette_path, cassette_dict, serializer):
cassette_path += ".test"
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 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
with my_vcr.use_cassette(str(tmpdir.join("nonexistent", "cassette.yml"))):
urlopen(httpbin.url).read()
# Callback should have made the file and the directory
assert os.path.exists(str(tmpdir.join("nonexistent", "cassette.yml.test")))
def test_load_cassette_with_custom_persister(tmpdir, httpbin):
"""
Ensure you can load a cassette using custom persister
"""
my_vcr = vcr.VCR()
my_vcr.register_persister(CustomFilesystemPersister)
test_fixture = str(tmpdir.join("synopsis.json.test"))
with my_vcr.use_cassette(test_fixture, serializer="json"):
response = urlopen(httpbin.url).read()
assert b"HTTP Request &amp; Response Service" 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), 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
@@ -10,7 +10,7 @@ class MockSerializer(object):
def deserialize(self, cassette_string):
self.serialize_count += 1
self.cassette_string = cassette_string
return {'interactions':[]}
return {"interactions": []}
def serialize(self, cassette_dict):
self.deserialize_count += 1
@@ -20,13 +20,13 @@ class MockSerializer(object):
def test_registered_serializer(tmpdir):
ms = MockSerializer()
my_vcr = vcr.VCR()
my_vcr.register_serializer('mock', ms)
tmpdir.join('test.mock').write('test_data')
with my_vcr.use_cassette(str(tmpdir.join('test.mock')), serializer='mock'):
my_vcr.register_serializer("mock", ms)
tmpdir.join("test.mock").write("test_data")
with my_vcr.use_cassette(str(tmpdir.join("test.mock")), serializer="mock"):
# Serializer deserialized once
assert ms.serialize_count == 1
# and serialized the test data string
assert ms.cassette_string == 'test_data'
assert ms.cassette_string == "test_data"
# and hasn't serialized yet
assert ms.deserialize_count == 0

View File

@@ -1,11 +1,20 @@
from urllib.request import urlopen
import vcr
from six.moves.urllib.request import urlopen
def test_recorded_request_uri_with_redirected_request(tmpdir):
with vcr.use_cassette(str(tmpdir.join('test.yml'))) as cass:
def test_recorded_request_uri_with_redirected_request(tmpdir, httpbin):
with vcr.use_cassette(str(tmpdir.join("test.yml"))) as cass:
assert len(cass) == 0
urlopen('http://httpbin.org/redirect/3')
assert cass.requests[0].uri == 'http://httpbin.org/redirect/3'
assert cass.requests[3].uri == 'http://httpbin.org/get'
urlopen(httpbin.url + "/redirect/3")
assert cass.requests[0].uri == httpbin.url + "/redirect/3"
assert cass.requests[3].uri == httpbin.url + "/get"
assert len(cass) == 4
def test_records_multiple_header_values(tmpdir, httpbin):
with vcr.use_cassette(str(tmpdir.join("test.yml"))) as cass:
assert len(cass) == 0
urlopen(httpbin.url + "/response-headers?foo=bar&foo=baz")
assert len(cass) == 1
assert cass.responses[0]["headers"]["foo"] == ["bar", "baz"]

View File

@@ -1,74 +1,85 @@
'''Test requests' interaction with vcr'''
"""Test requests' interaction with vcr"""
# coding=utf-8
import os
import pytest
import vcr
from assertions import (
assert_cassette_empty,
assert_cassette_has_one_response,
assert_is_json
)
from ..assertions import assert_cassette_empty, assert_is_json_bytes
requests = pytest.importorskip("requests")
@pytest.fixture(params=["https", "http"])
def scheme(request):
"""
Fixture that returns both http and https
"""
return request.param
def test_status_code(scheme, tmpdir):
'''Ensure that we can read the status code'''
url = scheme + '://httpbin.org/'
with vcr.use_cassette(str(tmpdir.join('atts.yaml'))) as cass:
def test_status_code(httpbin_both, tmpdir):
"""Ensure that we can read the status code"""
url = httpbin_both.url + "/"
with vcr.use_cassette(str(tmpdir.join("atts.yaml"))):
status_code = requests.get(url).status_code
with vcr.use_cassette(str(tmpdir.join('atts.yaml'))) as cass:
with vcr.use_cassette(str(tmpdir.join("atts.yaml"))):
assert status_code == requests.get(url).status_code
def test_headers(scheme, tmpdir):
'''Ensure that we can read the headers back'''
url = scheme + '://httpbin.org/'
with vcr.use_cassette(str(tmpdir.join('headers.yaml'))) as cass:
def test_headers(httpbin_both, tmpdir):
"""Ensure that we can read the headers back"""
url = httpbin_both + "/"
with vcr.use_cassette(str(tmpdir.join("headers.yaml"))):
headers = requests.get(url).headers
with vcr.use_cassette(str(tmpdir.join('headers.yaml'))) as cass:
with vcr.use_cassette(str(tmpdir.join("headers.yaml"))):
assert headers == requests.get(url).headers
def test_body(tmpdir, scheme):
'''Ensure the responses are all identical enough'''
url = scheme + '://httpbin.org/bytes/1024'
with vcr.use_cassette(str(tmpdir.join('body.yaml'))) as cass:
def test_body(tmpdir, httpbin_both):
"""Ensure the responses are all identical enough"""
url = httpbin_both + "/bytes/1024"
with vcr.use_cassette(str(tmpdir.join("body.yaml"))):
content = requests.get(url).content
with vcr.use_cassette(str(tmpdir.join('body.yaml'))) as cass:
with vcr.use_cassette(str(tmpdir.join("body.yaml"))):
assert content == requests.get(url).content
def test_auth(tmpdir, scheme):
'''Ensure that we can handle basic auth'''
auth = ('user', 'passwd')
url = scheme + '://httpbin.org/basic-auth/user/passwd'
with vcr.use_cassette(str(tmpdir.join('auth.yaml'))) as cass:
def test_get_empty_content_type_json(tmpdir, httpbin_both):
"""Ensure GET with application/json content-type and empty request body doesn't crash"""
url = httpbin_both + "/status/200"
headers = {"Content-Type": "application/json"}
with vcr.use_cassette(str(tmpdir.join("get_empty_json.yaml")), match_on=("body",)):
status = requests.get(url, headers=headers).status_code
with vcr.use_cassette(str(tmpdir.join("get_empty_json.yaml")), match_on=("body",)):
assert status == requests.get(url, headers=headers).status_code
def test_effective_url(tmpdir, httpbin_both):
"""Ensure that the effective_url is captured"""
url = httpbin_both.url + "/redirect-to?url=/html"
with vcr.use_cassette(str(tmpdir.join("url.yaml"))):
effective_url = requests.get(url).url
assert effective_url == httpbin_both.url + "/html"
with vcr.use_cassette(str(tmpdir.join("url.yaml"))):
assert effective_url == requests.get(url).url
def test_auth(tmpdir, httpbin_both):
"""Ensure that we can handle basic auth"""
auth = ("user", "passwd")
url = httpbin_both + "/basic-auth/user/passwd"
with vcr.use_cassette(str(tmpdir.join("auth.yaml"))):
one = requests.get(url, auth=auth)
with vcr.use_cassette(str(tmpdir.join('auth.yaml'))) as cass:
with vcr.use_cassette(str(tmpdir.join("auth.yaml"))):
two = requests.get(url, auth=auth)
assert one.content == two.content
assert one.status_code == two.status_code
def test_auth_failed(tmpdir, scheme):
'''Ensure that we can save failed auth statuses'''
auth = ('user', 'wrongwrongwrong')
url = scheme + '://httpbin.org/basic-auth/user/passwd'
with vcr.use_cassette(str(tmpdir.join('auth-failed.yaml'))) as cass:
def test_auth_failed(tmpdir, httpbin_both):
"""Ensure that we can save failed auth statuses"""
auth = ("user", "wrongwrongwrong")
url = httpbin_both + "/basic-auth/user/passwd"
with vcr.use_cassette(str(tmpdir.join("auth-failed.yaml"))) as cass:
# Ensure that this is empty to begin with
assert_cassette_empty(cass)
one = requests.get(url, auth=auth)
@@ -77,26 +88,40 @@ def test_auth_failed(tmpdir, scheme):
assert one.status_code == two.status_code == 401
def test_post(tmpdir, scheme):
'''Ensure that we can post and cache the results'''
data = {'key1': 'value1', 'key2': 'value2'}
url = scheme + '://httpbin.org/post'
with vcr.use_cassette(str(tmpdir.join('requests.yaml'))) as cass:
def test_post(tmpdir, httpbin_both):
"""Ensure that we can post and cache the results"""
data = {"key1": "value1", "key2": "value2"}
url = httpbin_both + "/post"
with vcr.use_cassette(str(tmpdir.join("requests.yaml"))):
req1 = requests.post(url, data).content
with vcr.use_cassette(str(tmpdir.join('requests.yaml'))) as cass:
with vcr.use_cassette(str(tmpdir.join("requests.yaml"))):
req2 = requests.post(url, data).content
assert req1 == req2
def test_redirects(tmpdir, scheme):
'''Ensure that we can handle redirects'''
url = scheme + '://httpbin.org/redirect-to?url=bytes/1024'
with vcr.use_cassette(str(tmpdir.join('requests.yaml'))) as cass:
def test_post_chunked_binary(tmpdir, httpbin):
"""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.url + "/post"
with vcr.use_cassette(str(tmpdir.join("requests.yaml"))):
req1 = requests.post(url, data1).content
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"
with vcr.use_cassette(str(tmpdir.join("requests.yaml"))):
content = requests.get(url).content
with vcr.use_cassette(str(tmpdir.join('requests.yaml'))) as cass:
with vcr.use_cassette(str(tmpdir.join("requests.yaml"))) as cass:
assert content == requests.get(url).content
# Ensure that we've now cached *two* responses. One for the redirect
# and one for the final fetch
@@ -104,44 +129,184 @@ def test_redirects(tmpdir, scheme):
assert cass.play_count == 2
def test_cross_scheme(tmpdir, scheme):
'''Ensure that requests between schemes are treated separately'''
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
# ensure that we haven't served anything out of cache, and we have two
# requests / response pairs in the cassette
with vcr.use_cassette(str(tmpdir.join('cross_scheme.yaml'))) as cass:
requests.get('https://httpbin.org/')
requests.get('http://httpbin.org/')
with vcr.use_cassette(str(tmpdir.join("cross_scheme.yaml"))) as cass:
requests.get(httpbin_secure + "/")
requests.get(httpbin + "/")
assert cass.play_count == 0
assert len(cass) == 2
def test_gzip(tmpdir, scheme):
'''
def test_gzip__decode_compressed_response_false(tmpdir, httpbin_both):
"""
Ensure that requests (actually urllib3) is able to automatically decompress
the response body
'''
url = scheme + '://httpbin.org/gzip'
response = requests.get(url)
with vcr.use_cassette(str(tmpdir.join('gzip.yaml'))) as cass:
response = requests.get(url)
assert_is_json(response.content)
with vcr.use_cassette(str(tmpdir.join('gzip.yaml'))) as cass:
assert_is_json(response.content)
"""
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_session_and_connection_close(tmpdir, scheme):
'''
def test_gzip__decode_compressed_response_true(tmpdir, httpbin_both):
url = httpbin_both + "/gzip"
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("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):
"""
This tests the issue in https://github.com/kevin1024/vcrpy/issues/48
If you use a requests.session and the connection is closed, then an
exception is raised in the urllib3 module vendored into requests:
`AttributeError: 'NoneType' object has no attribute 'settimeout'`
'''
with vcr.use_cassette(str(tmpdir.join('session_connection_closed.yaml'))):
"""
with vcr.use_cassette(str(tmpdir.join("session_connection_closed.yaml"))):
session = requests.session()
resp = session.get('http://httpbin.org/get', headers={'Connection': 'close'})
resp = session.get('http://httpbin.org/get', headers={'Connection': 'close'})
session.get(httpbin + "/get", headers={"Connection": "close"})
session.get(httpbin + "/get", headers={"Connection": "close"})
def test_https_with_cert_validation_disabled(tmpdir, httpbin_secure):
with vcr.use_cassette(str(tmpdir.join("cert_validation_disabled.yaml"))):
requests.get(httpbin_secure.url, verify=False)
def test_session_can_make_requests_after_requests_unpatched(tmpdir, httpbin):
with vcr.use_cassette(str(tmpdir.join("test_session_after_unpatched.yaml"))):
session = requests.session()
session.get(httpbin + "/get")
with vcr.use_cassette(str(tmpdir.join("test_session_after_unpatched.yaml"))):
session = requests.session()
session.get(httpbin + "/get")
session.get(httpbin + "/status/200")
def test_session_created_before_use_cassette_is_patched(tmpdir, httpbin_both):
url = httpbin_both + "/bytes/1024"
# Record arbitrary, random data to the cassette
with vcr.use_cassette(str(tmpdir.join("session_created_outside.yaml"))):
session = requests.session()
body = session.get(url).content
# Create a session outside of any cassette context manager
session = requests.session()
# Make a request to make sure that a connectionpool is instantiated
session.get(httpbin_both + "/get")
with vcr.use_cassette(str(tmpdir.join("session_created_outside.yaml"))):
# These should only be the same if the patching succeeded.
assert session.get(url).content == body
def test_nested_cassettes_with_session_created_before_nesting(httpbin_both, tmpdir):
"""
This tests ensures that a session that was created while one cassette was
active is patched to the use the responses of a second cassette when it
is enabled.
"""
url = httpbin_both + "/bytes/1024"
with vcr.use_cassette(str(tmpdir.join("first_nested.yaml"))):
session = requests.session()
first_body = session.get(url).content
with vcr.use_cassette(str(tmpdir.join("second_nested.yaml"))):
second_body = session.get(url).content
third_body = requests.get(url).content
with vcr.use_cassette(str(tmpdir.join("second_nested.yaml"))):
session = requests.session()
assert session.get(url).content == second_body
with vcr.use_cassette(str(tmpdir.join("first_nested.yaml"))):
assert session.get(url).content == first_body
assert session.get(url).content == third_body
# Make sure that the session can now get content normally.
assert "User-agent" in session.get(httpbin_both.url + "/robots.txt").text
def test_post_file(tmpdir, httpbin_both):
"""Ensure that we handle posting a file."""
url = httpbin_both + "/post"
with vcr.use_cassette(str(tmpdir.join("post_file.yaml"))) as cass, open(".editorconfig", "rb") as f:
original_response = requests.post(url, f).content
# This also tests that we do the right thing with matching the body when they are files.
with vcr.use_cassette(
str(tmpdir.join("post_file.yaml")),
match_on=("method", "scheme", "host", "port", "path", "query", "body"),
) as cass:
with open(".editorconfig", "rb") as f:
editorconfig = f.read()
assert cass.requests[0].body.read() == editorconfig
with open(".editorconfig", "rb") as f:
new_response = requests.post(url, f).content
assert original_response == new_response
def test_filter_post_params(tmpdir, httpbin_both):
"""
This tests the issue in https://github.com/kevin1024/vcrpy/issues/158
Ensure that a post request made through requests can still be filtered.
with vcr.use_cassette(cass_file, filter_post_data_parameters=['id']) as cass:
assert b'id=secret' not in cass.requests[0].body
"""
url = httpbin_both.url + "/post"
cass_loc = str(tmpdir.join("filter_post_params.yaml"))
with vcr.use_cassette(cass_loc, filter_post_data_parameters=["key"]) as cass:
requests.post(url, data={"key": "value"})
with vcr.use_cassette(cass_loc, filter_post_data_parameters=["key"]) as cass:
assert b"key=value" not in cass.requests[0].body
def test_post_unicode_match_on_body(tmpdir, httpbin_both):
"""Ensure that matching on POST body that contains Unicode characters works."""
data = {"key1": "value1", "●‿●": "٩(●̮̮̃•̃)۶"}
url = httpbin_both + "/post"
with vcr.use_cassette(str(tmpdir.join("requests.yaml")), additional_matchers=("body",)):
req1 = requests.post(url, data).content
with vcr.use_cassette(str(tmpdir.join("requests.yaml")), additional_matchers=("body",)):
req2 = requests.post(url, data).content
assert req1 == req2

View File

@@ -0,0 +1,135 @@
import http.client as httplib
import json
import zlib
import vcr
from ..assertions import assert_is_json_bytes
def _headers_are_case_insensitive(host, port):
conn = httplib.HTTPConnection(host, port)
conn.request("GET", "/cookies/set?k1=v1")
r1 = conn.getresponse()
cookie_data1 = r1.getheader("set-cookie")
conn = httplib.HTTPConnection(host, port)
conn.request("GET", "/cookies/set?k1=v1")
r2 = conn.getresponse()
cookie_data2 = r2.getheader("Set-Cookie")
return cookie_data1 == cookie_data2
def test_case_insensitivity(tmpdir, httpbin):
testfile = str(tmpdir.join("case_insensitivity.yml"))
# check if headers are case insensitive outside of vcrpy
host, port = httpbin.host, httpbin.port
outside = _headers_are_case_insensitive(host, port)
with vcr.use_cassette(testfile):
# check if headers are case insensitive inside of vcrpy
inside = _headers_are_case_insensitive(host, port)
# check if headers are case insensitive after vcrpy deserializes headers
inside2 = _headers_are_case_insensitive(host, port)
# behavior should be the same both inside and outside
assert outside == inside == inside2
def _multiple_header_value(httpbin):
conn = httplib.HTTPConnection(httpbin.host, httpbin.port)
conn.request("GET", "/response-headers?foo=bar&foo=baz")
r = conn.getresponse()
return r.getheader("foo")
def test_multiple_headers(tmpdir, httpbin):
testfile = str(tmpdir.join("multiple_headers.yaml"))
outside = _multiple_header_value(httpbin)
with vcr.use_cassette(testfile):
inside = _multiple_header_value(httpbin)
assert outside == inside
def test_original_decoded_response_is_not_modified(tmpdir, httpbin):
testfile = str(tmpdir.join("decoded_response.yml"))
host, port = httpbin.host, httpbin.port
conn = httplib.HTTPConnection(host, port)
conn.request("GET", "/gzip")
outside = conn.getresponse()
with vcr.use_cassette(testfile, decode_compressed_response=True):
conn = httplib.HTTPConnection(host, port)
conn.request("GET", "/gzip")
inside = conn.getresponse()
# Assert that we do not modify the original response while appending
# to the cassette.
assert inside.headers["content-encoding"] == "gzip"
# They should effectively be the same response.
inside_headers = (h for h in inside.headers.items() if h[0].lower() != "date")
outside_headers = (h for h in outside.getheaders() if h[0].lower() != "date")
assert set(inside_headers) == set(outside_headers)
inside = zlib.decompress(inside.read(), 16 + zlib.MAX_WBITS)
outside = zlib.decompress(outside.read(), 16 + zlib.MAX_WBITS)
assert inside == outside
# Even though the above are raw bytes, the JSON data should have been
# decoded and saved to the cassette.
with vcr.use_cassette(testfile):
conn = httplib.HTTPConnection(host, port)
conn.request("GET", "/gzip")
inside = conn.getresponse()
assert "content-encoding" not in inside.headers
assert_is_json_bytes(inside.read())
def _make_before_record_response(fields, replacement="[REDACTED]"):
def before_record_response(response):
string_body = response["body"]["string"].decode("utf8")
body = json.loads(string_body)
for field in fields:
if field in body:
body[field] = replacement
response["body"]["string"] = json.dumps(body).encode()
return response
return before_record_response
def test_original_response_is_not_modified_by_before_filter(tmpdir, httpbin):
testfile = str(tmpdir.join("sensitive_data_scrubbed_response.yml"))
host, port = httpbin.host, httpbin.port
field_to_scrub = "url"
replacement = "[YOU_CANT_HAVE_THE_MANGO]"
conn = httplib.HTTPConnection(host, port)
conn.request("GET", "/get")
outside = conn.getresponse()
callback = _make_before_record_response([field_to_scrub], replacement)
with vcr.use_cassette(testfile, before_record_response=callback):
conn = httplib.HTTPConnection(host, port)
conn.request("GET", "/get")
inside = conn.getresponse()
# The scrubbed field should be the same, because no cassette existed.
# Furthermore, the responses should be identical.
inside_body = json.loads(inside.read())
outside_body = json.loads(outside.read())
assert inside_body[field_to_scrub] != replacement
assert inside_body[field_to_scrub] == outside_body[field_to_scrub]
# Ensure that when a cassette exists, the scrubbed response is returned.
with vcr.use_cassette(testfile, before_record_response=callback):
conn = httplib.HTTPConnection(host, port)
conn.request("GET", "/get")
inside = conn.getresponse()
inside_body = json.loads(inside.read())
assert inside_body[field_to_scrub] == replacement

View File

@@ -0,0 +1,410 @@
"""Test requests' interaction with vcr"""
import asyncio
import functools
import inspect
import json
import os
import ssl
import pytest
import vcr
from vcr.errors import CannotOverwriteExistingCassetteException
from ..assertions import assert_cassette_empty, assert_is_json_bytes
tornado = pytest.importorskip("tornado")
gen = pytest.importorskip("tornado.gen")
http = pytest.importorskip("tornado.httpclient")
# whether the current version of Tornado supports the raise_error argument for
# fetch().
supports_raise_error = tornado.version_info >= (4,)
raise_error_for_response_code_only = tornado.version_info >= (6,)
def gen_test(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
async def coro():
return await gen.coroutine(func)(*args, **kwargs)
return asyncio.run(coro())
# Patch the signature so pytest can inject fixtures
# we can't use wrapt.decorator because it returns a generator function
wrapper.__signature__ = inspect.signature(func)
return wrapper
@pytest.fixture(params=["simple", "curl", "default"])
def get_client(request):
ca_bundle_path = os.environ.get("REQUESTS_CA_BUNDLE")
ssl_ctx = ssl.create_default_context(ssl.Purpose.SERVER_AUTH)
ssl_ctx.load_verify_locations(cafile=ca_bundle_path)
ssl_ctx.verify_mode = ssl.CERT_REQUIRED
if request.param == "simple":
from tornado import simple_httpclient as simple
return lambda: simple.SimpleAsyncHTTPClient(defaults={"ssl_options": ssl_ctx})
if request.param == "curl":
curl = pytest.importorskip("tornado.curl_httpclient")
return lambda: curl.CurlAsyncHTTPClient(defaults={"ca_certs": ca_bundle_path})
return lambda: http.AsyncHTTPClient(defaults={"ssl_options": ssl_ctx})
def get(client, url, **kwargs):
fetch_kwargs = {}
if supports_raise_error:
fetch_kwargs["raise_error"] = kwargs.pop("raise_error", True)
return client.fetch(http.HTTPRequest(url, method="GET", **kwargs), **fetch_kwargs)
def post(client, url, data=None, **kwargs):
if data:
kwargs["body"] = json.dumps(data)
return client.fetch(http.HTTPRequest(url, method="POST", **kwargs))
@pytest.mark.online
@gen_test
def test_status_code(get_client, tmpdir, httpbin_both):
"""Ensure that we can read the status code"""
url = httpbin_both.url
with vcr.use_cassette(str(tmpdir.join("atts.yaml"))):
status_code = (yield get(get_client(), url)).code
with vcr.use_cassette(str(tmpdir.join("atts.yaml"))) as cass:
assert status_code == (yield get(get_client(), url)).code
assert cass.play_count == 1
@pytest.mark.online
@gen_test
def test_headers(get_client, httpbin_both, tmpdir):
"""Ensure that we can read the headers back"""
url = httpbin_both.url
with vcr.use_cassette(str(tmpdir.join("headers.yaml"))):
headers = (yield get(get_client(), url)).headers
with vcr.use_cassette(str(tmpdir.join("headers.yaml"))) as cass:
assert headers == (yield get(get_client(), url)).headers
assert cass.play_count == 1
@pytest.mark.online
@gen_test
def test_body(get_client, tmpdir, httpbin_both):
"""Ensure the responses are all identical enough"""
url = httpbin_both.url + "/bytes/1024"
with vcr.use_cassette(str(tmpdir.join("body.yaml"))):
content = (yield get(get_client(), url)).body
with vcr.use_cassette(str(tmpdir.join("body.yaml"))) as cass:
assert content == (yield get(get_client(), url)).body
assert cass.play_count == 1
@gen_test
def test_effective_url(get_client, tmpdir, httpbin):
"""Ensure that the effective_url is captured"""
url = httpbin.url + "/redirect/1"
with vcr.use_cassette(str(tmpdir.join("url.yaml"))):
effective_url = (yield get(get_client(), url)).effective_url
assert effective_url == httpbin.url + "/get"
with vcr.use_cassette(str(tmpdir.join("url.yaml"))) as cass:
assert effective_url == (yield get(get_client(), url)).effective_url
assert cass.play_count == 1
@pytest.mark.online
@gen_test
def test_auth(get_client, tmpdir, httpbin_both):
"""Ensure that we can handle basic auth"""
auth = ("user", "passwd")
url = httpbin_both.url + "/basic-auth/user/passwd"
with vcr.use_cassette(str(tmpdir.join("auth.yaml"))):
one = yield get(get_client(), url, auth_username=auth[0], auth_password=auth[1])
with vcr.use_cassette(str(tmpdir.join("auth.yaml"))) as cass:
two = yield get(get_client(), url, auth_username=auth[0], auth_password=auth[1])
assert one.body == two.body
assert one.code == two.code
assert cass.play_count == 1
@pytest.mark.online
@gen_test
def test_auth_failed(get_client, tmpdir, httpbin_both):
"""Ensure that we can save failed auth statuses"""
auth = ("user", "wrongwrongwrong")
url = httpbin_both.url + "/basic-auth/user/passwd"
with vcr.use_cassette(str(tmpdir.join("auth-failed.yaml"))) as cass:
# Ensure that this is empty to begin with
assert_cassette_empty(cass)
with pytest.raises(http.HTTPError) as exc_info:
yield get(get_client(), url, auth_username=auth[0], auth_password=auth[1])
one = exc_info.value.response
assert exc_info.value.code == 401
with vcr.use_cassette(str(tmpdir.join("auth-failed.yaml"))) as cass:
with pytest.raises(http.HTTPError) as exc_info:
two = yield get(get_client(), url, auth_username=auth[0], auth_password=auth[1])
two = exc_info.value.response
assert exc_info.value.code == 401
assert one.body == two.body
assert one.code == two.code == 401
assert cass.play_count == 1
@pytest.mark.online
@gen_test
def test_post(get_client, tmpdir, httpbin_both):
"""Ensure that we can post and cache the results"""
data = {"key1": "value1", "key2": "value2"}
url = httpbin_both.url + "/post"
with vcr.use_cassette(str(tmpdir.join("requests.yaml"))):
req1 = (yield post(get_client(), url, data)).body
with vcr.use_cassette(str(tmpdir.join("requests.yaml"))) as cass:
req2 = (yield post(get_client(), url, data)).body
assert req1 == req2
assert cass.play_count == 1
@gen_test
def test_redirects(get_client, tmpdir, httpbin):
"""Ensure that we can handle redirects"""
url = httpbin + "/redirect-to?url=bytes/1024&status_code=301"
with vcr.use_cassette(str(tmpdir.join("requests.yaml"))):
content = (yield get(get_client(), url)).body
with vcr.use_cassette(str(tmpdir.join("requests.yaml"))) as cass:
assert content == (yield get(get_client(), url)).body
assert cass.play_count == 1
@pytest.mark.online
@gen_test
def test_cross_scheme(get_client, tmpdir, httpbin, httpbin_secure):
"""Ensure that requests between schemes are treated separately"""
# First fetch a url under http, and then again under https and then
# ensure that we haven't served anything out of cache, and we have two
# requests / response pairs in the cassette
url = httpbin.url
url_secure = httpbin_secure.url
with vcr.use_cassette(str(tmpdir.join("cross_scheme.yaml"))) as cass:
yield get(get_client(), url)
yield get(get_client(), url_secure)
assert cass.play_count == 0
assert len(cass) == 2
# Then repeat the same requests and ensure both were replayed.
with vcr.use_cassette(str(tmpdir.join("cross_scheme.yaml"))) as cass:
yield get(get_client(), url)
yield get(get_client(), url_secure)
assert cass.play_count == 2
@pytest.mark.online
@gen_test
def test_gzip(get_client, tmpdir, httpbin_both):
"""
Ensure that httpclient is able to automatically decompress the response
body
"""
url = httpbin_both + "/gzip"
# use_gzip was renamed to decompress_response in 4.0
kwargs = {}
if tornado.version_info < (4,):
kwargs["use_gzip"] = True
else:
kwargs["decompress_response"] = True
with vcr.use_cassette(str(tmpdir.join("gzip.yaml"))):
response = yield get(get_client(), url, **kwargs)
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_bytes(response.body)
assert cass.play_count == 1
@pytest.mark.online
@gen_test
def test_https_with_cert_validation_disabled(get_client, tmpdir, httpbin_secure):
cass_path = str(tmpdir.join("cert_validation_disabled.yaml"))
url = httpbin_secure.url
with vcr.use_cassette(cass_path):
yield get(get_client(), url, validate_cert=False)
with vcr.use_cassette(cass_path) as cass:
yield get(get_client(), url, validate_cert=False)
assert cass.play_count == 1
@gen_test
def test_unsupported_features_raises_in_future(get_client, tmpdir, httpbin):
"""Ensure that the exception for an AsyncHTTPClient feature not being
supported is raised inside the future."""
def callback(chunk):
raise AssertionError("Did not expect to be called.")
with vcr.use_cassette(str(tmpdir.join("invalid.yaml"))):
future = get(get_client(), httpbin.url, streaming_callback=callback)
with pytest.raises(Exception) as excinfo:
yield future
assert "not yet supported by VCR" in str(excinfo)
@pytest.mark.skipif(not supports_raise_error, reason="raise_error unavailable in tornado <= 3")
@pytest.mark.skipif(
raise_error_for_response_code_only,
reason="raise_error only ignores HTTPErrors due to response code",
)
@gen_test
def test_unsupported_features_raise_error_disabled(get_client, tmpdir):
"""Ensure that the exception for an AsyncHTTPClient feature not being
supported is not raised if raise_error=False."""
def callback(chunk):
raise AssertionError("Did not expect to be called.")
with vcr.use_cassette(str(tmpdir.join("invalid.yaml"))):
response = yield get(
get_client(),
"http://httpbin.org",
streaming_callback=callback,
raise_error=False,
)
assert "not yet supported by VCR" in str(response.error)
@pytest.mark.online
@gen_test
def test_cannot_overwrite_cassette_raises_in_future(get_client, tmpdir, httpbin):
"""Ensure that CannotOverwriteExistingCassetteException is raised inside
the future."""
url = httpbin.url
with vcr.use_cassette(str(tmpdir.join("overwrite.yaml"))):
yield get(get_client(), url + "/get")
with vcr.use_cassette(str(tmpdir.join("overwrite.yaml"))):
future = get(get_client(), url + "/headers")
with pytest.raises(CannotOverwriteExistingCassetteException):
yield future
@pytest.mark.skipif(not supports_raise_error, reason="raise_error unavailable in tornado <= 3")
@pytest.mark.skipif(
raise_error_for_response_code_only,
reason="raise_error only ignores HTTPErrors due to response code",
)
@gen_test
def test_cannot_overwrite_cassette_raise_error_disabled(get_client, tmpdir, httpbin):
"""Ensure that CannotOverwriteExistingCassetteException is not raised if
raise_error=False in the fetch() call."""
url = httpbin.url
with vcr.use_cassette(str(tmpdir.join("overwrite.yaml"))):
yield get(get_client(), url + "/get", raise_error=False)
with vcr.use_cassette(str(tmpdir.join("overwrite.yaml"))):
response = yield get(get_client(), url + "/headers", raise_error=False)
assert isinstance(response.error, CannotOverwriteExistingCassetteException)
@gen_test
@vcr.use_cassette(path_transformer=vcr.default_vcr.ensure_suffix(".yaml"))
def test_tornado_with_decorator_use_cassette(get_client):
response = yield get_client().fetch(http.HTTPRequest("http://www.google.com/", method="GET"))
assert response.body.decode("utf-8") == "not actually google"
@gen_test
@vcr.use_cassette(path_transformer=vcr.default_vcr.ensure_suffix(".yaml"))
def test_tornado_exception_can_be_caught(get_client):
try:
yield get(get_client(), "http://httpbin.org/status/500")
except http.HTTPError as e:
assert e.code == 500
try:
yield get(get_client(), "http://httpbin.org/status/404")
except http.HTTPError as e:
assert e.code == 404
@pytest.mark.online
@gen_test
def test_existing_references_get_patched(tmpdir, httpbin):
from tornado.httpclient import AsyncHTTPClient
url = httpbin.url + "/get"
with vcr.use_cassette(str(tmpdir.join("data.yaml"))):
client = AsyncHTTPClient()
yield get(client, url)
with vcr.use_cassette(str(tmpdir.join("data.yaml"))) as cass:
yield get(client, url)
assert cass.play_count == 1
@pytest.mark.online
@gen_test
def test_existing_instances_get_patched(get_client, tmpdir, httpbin):
"""Ensure that existing instances of AsyncHTTPClient get patched upon
entering VCR context."""
url = httpbin.url + "/get"
client = get_client()
with vcr.use_cassette(str(tmpdir.join("data.yaml"))):
yield get(client, url)
with vcr.use_cassette(str(tmpdir.join("data.yaml"))) as cass:
yield get(client, url)
assert cass.play_count == 1
@pytest.mark.online
@gen_test
def test_request_time_is_set(get_client, tmpdir, httpbin):
"""Ensures that the request_time on HTTPResponses is set."""
url = httpbin.url + "/get"
with vcr.use_cassette(str(tmpdir.join("data.yaml"))):
client = get_client()
response = yield get(client, url)
assert response.request_time is not None
with vcr.use_cassette(str(tmpdir.join("data.yaml"))) as cass:
client = get_client()
response = yield get(client, url)
assert response.request_time is not None
assert cass.play_count == 1

View File

@@ -0,0 +1,62 @@
interactions:
- request:
body: null
headers: {}
method: GET
uri: http://httpbin.org/status/500
response:
body: {string: !!python/unicode ''}
headers:
- !!python/tuple
- Content-Length
- ['0']
- !!python/tuple
- Server
- [nginx]
- !!python/tuple
- Connection
- [close]
- !!python/tuple
- Access-Control-Allow-Credentials
- ['true']
- !!python/tuple
- Date
- ['Thu, 30 Jul 2015 17:32:39 GMT']
- !!python/tuple
- Access-Control-Allow-Origin
- ['*']
- !!python/tuple
- Content-Type
- [text/html; charset=utf-8]
status: {code: 500, message: INTERNAL SERVER ERROR}
- request:
body: null
headers: {}
method: GET
uri: http://httpbin.org/status/404
response:
body: {string: !!python/unicode ''}
headers:
- !!python/tuple
- Content-Length
- ['0']
- !!python/tuple
- Server
- [nginx]
- !!python/tuple
- Connection
- [close]
- !!python/tuple
- Access-Control-Allow-Credentials
- ['true']
- !!python/tuple
- Date
- ['Thu, 30 Jul 2015 17:32:39 GMT']
- !!python/tuple
- Access-Control-Allow-Origin
- ['*']
- !!python/tuple
- Content-Type
- [text/html; charset=utf-8]
status: {code: 404, message: NOT FOUND}
version: 1

View File

@@ -0,0 +1,53 @@
interactions:
- request:
body: null
headers: {}
method: GET
uri: http://www.google.com/
response:
body: {string: !!python/unicode 'not actually google'}
headers:
- !!python/tuple
- Expires
- ['-1']
- !!python/tuple
- Connection
- [close]
- !!python/tuple
- P3p
- ['CP="This is not a P3P policy! See http://www.google.com/support/accounts/bin/answer.py?hl=en&answer=151657
for more info."']
- !!python/tuple
- Alternate-Protocol
- ['80:quic,p=0']
- !!python/tuple
- Accept-Ranges
- [none]
- !!python/tuple
- X-Xss-Protection
- [1; mode=block]
- !!python/tuple
- Vary
- [Accept-Encoding]
- !!python/tuple
- Date
- ['Thu, 30 Jul 2015 08:41:40 GMT']
- !!python/tuple
- Cache-Control
- ['private, max-age=0']
- !!python/tuple
- Content-Type
- [text/html; charset=ISO-8859-1]
- !!python/tuple
- Set-Cookie
- ['PREF=ID=1111111111111111:FF=0:TM=1438245700:LM=1438245700:V=1:S=GAzVO0ALebSpC_cJ;
expires=Sat, 29-Jul-2017 08:41:40 GMT; path=/; domain=.google.com', 'NID=69=Br7oRAwgmKoK__HC6FEnuxglTFDmFxqP6Md63lKhzW1w6WkDbp3U90CDxnUKvDP6wJH8yxY5Lk5ZnFf66Q1B0d4OsYoKgq0vjfBAYXuCIAWtOuGZEOsFXanXs7pt2Mjx;
expires=Fri, 29-Jan-2016 08:41:40 GMT; path=/; domain=.google.com; HttpOnly']
- !!python/tuple
- X-Frame-Options
- [SAMEORIGIN]
- !!python/tuple
- Server
- [gws]
status: {code: 200, message: OK}
version: 1

View File

@@ -1,131 +0,0 @@
'''Integration tests with urllib2'''
# coding=utf-8
# External imports
import os
import pytest
from six.moves.urllib.request import urlopen
from six.moves.urllib_parse import urlencode
# Internal imports
import vcr
from assertions import assert_cassette_empty, assert_cassette_has_one_response
@pytest.fixture(params=["https", "http"])
def scheme(request):
"""
Fixture that returns both http and https
"""
return request.param
def test_response_code(scheme, tmpdir):
'''Ensure we can read a response code from a fetch'''
url = scheme + '://httpbin.org/'
with vcr.use_cassette(str(tmpdir.join('atts.yaml'))) as cass:
code = urlopen(url).getcode()
with vcr.use_cassette(str(tmpdir.join('atts.yaml'))) as cass:
assert code == urlopen(url).getcode()
def test_random_body(scheme, tmpdir):
'''Ensure we can read the content, and that it's served from cache'''
url = scheme + '://httpbin.org/bytes/1024'
with vcr.use_cassette(str(tmpdir.join('body.yaml'))) as cass:
body = urlopen(url).read()
with vcr.use_cassette(str(tmpdir.join('body.yaml'))) as cass:
assert body == urlopen(url).read()
def test_response_headers(scheme, tmpdir):
'''Ensure we can get information from the response'''
url = scheme + '://httpbin.org/'
with vcr.use_cassette(str(tmpdir.join('headers.yaml'))) as cass:
open1 = urlopen(url).info().items()
with vcr.use_cassette(str(tmpdir.join('headers.yaml'))) as cass:
open2 = urlopen(url).info().items()
assert sorted(open1) == sorted(open2)
def test_multiple_requests(scheme, tmpdir):
'''Ensure that we can cache multiple requests'''
urls = [
scheme + '://httpbin.org/',
scheme + '://httpbin.org/',
scheme + '://httpbin.org/get',
scheme + '://httpbin.org/bytes/1024'
]
with vcr.use_cassette(str(tmpdir.join('multiple.yaml'))) as cass:
[urlopen(url) for url in urls]
assert len(cass) == len(urls)
def test_get_data(scheme, tmpdir):
'''Ensure that it works with query data'''
data = urlencode({'some': 1, 'data': 'here'})
url = scheme + '://httpbin.org/get?' + data
with vcr.use_cassette(str(tmpdir.join('get_data.yaml'))) as cass:
res1 = urlopen(url).read()
with vcr.use_cassette(str(tmpdir.join('get_data.yaml'))) as cass:
res2 = urlopen(url).read()
assert res1 == res2
def test_post_data(scheme, tmpdir):
'''Ensure that it works when posting data'''
data = urlencode({'some': 1, 'data': 'here'}).encode('utf-8')
url = scheme + '://httpbin.org/post'
with vcr.use_cassette(str(tmpdir.join('post_data.yaml'))) as cass:
res1 = urlopen(url, data).read()
with vcr.use_cassette(str(tmpdir.join('post_data.yaml'))) as cass:
res2 = urlopen(url, data).read()
assert res1 == res2
assert_cassette_has_one_response(cass)
def test_post_unicode_data(scheme, tmpdir):
'''Ensure that it works when posting unicode data'''
data = urlencode({'snowman': u''.encode('utf-8')}).encode('utf-8')
url = scheme + '://httpbin.org/post'
with vcr.use_cassette(str(tmpdir.join('post_data.yaml'))) as cass:
res1 = urlopen(url, data).read()
with vcr.use_cassette(str(tmpdir.join('post_data.yaml'))) as cass:
res2 = urlopen(url, data).read()
assert res1 == res2
assert_cassette_has_one_response(cass)
def test_cross_scheme(tmpdir):
'''Ensure that requests between schemes are treated separately'''
# First fetch a url under https, and then again under https and then
# ensure that we haven't served anything out of cache, and we have two
# requests / response pairs in the cassette
with vcr.use_cassette(str(tmpdir.join('cross_scheme.yaml'))) as cass:
urlopen('https://httpbin.org/')
urlopen('http://httpbin.org/')
assert len(cass) == 2
assert cass.play_count == 0
def test_decorator(scheme, tmpdir):
'''Test the decorator version of VCR.py'''
url = scheme + '://httpbin.org/'
@vcr.use_cassette(str(tmpdir.join('atts.yaml')))
def inner1():
return urlopen(url).getcode()
@vcr.use_cassette(str(tmpdir.join('atts.yaml')))
def inner2():
return urlopen(url).getcode()
assert inner1() == inner2()

View File

@@ -0,0 +1,167 @@
"""Integration tests with urllib3"""
# coding=utf-8
import pytest
import pytest_httpbin
import vcr
from vcr.patch import force_reset
from vcr.stubs.compat import get_headers
from ..assertions import assert_cassette_empty, assert_is_json_bytes
urllib3 = pytest.importorskip("urllib3")
@pytest.fixture(scope="module")
def verify_pool_mgr():
return urllib3.PoolManager(
cert_reqs="CERT_REQUIRED",
ca_certs=pytest_httpbin.certs.where(), # Force certificate check.
)
@pytest.fixture(scope="module")
def pool_mgr():
return urllib3.PoolManager(cert_reqs="CERT_NONE")
def test_status_code(httpbin_both, tmpdir, verify_pool_mgr):
"""Ensure that we can read the status code"""
url = httpbin_both.url
with vcr.use_cassette(str(tmpdir.join("atts.yaml"))):
status_code = verify_pool_mgr.request("GET", url).status
with vcr.use_cassette(str(tmpdir.join("atts.yaml"))):
assert status_code == verify_pool_mgr.request("GET", url).status
def test_headers(tmpdir, httpbin_both, verify_pool_mgr):
"""Ensure that we can read the headers back"""
url = httpbin_both.url
with vcr.use_cassette(str(tmpdir.join("headers.yaml"))):
headers = verify_pool_mgr.request("GET", url).headers
with vcr.use_cassette(str(tmpdir.join("headers.yaml"))):
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):
"""Ensure the responses are all identical enough"""
url = httpbin_both.url + "/bytes/1024"
with vcr.use_cassette(str(tmpdir.join("body.yaml"))):
content = verify_pool_mgr.request("GET", url).data
with vcr.use_cassette(str(tmpdir.join("body.yaml"))):
assert content == verify_pool_mgr.request("GET", url).data
def test_auth(tmpdir, httpbin_both, verify_pool_mgr):
"""Ensure that we can handle basic auth"""
auth = ("user", "passwd")
headers = urllib3.util.make_headers(basic_auth="{}:{}".format(*auth))
url = httpbin_both.url + "/basic-auth/user/passwd"
with vcr.use_cassette(str(tmpdir.join("auth.yaml"))):
one = verify_pool_mgr.request("GET", url, headers=headers)
with vcr.use_cassette(str(tmpdir.join("auth.yaml"))):
two = verify_pool_mgr.request("GET", url, headers=headers)
assert one.data == two.data
assert one.status == two.status
def test_auth_failed(tmpdir, httpbin_both, verify_pool_mgr):
"""Ensure that we can save failed auth statuses"""
auth = ("user", "wrongwrongwrong")
headers = urllib3.util.make_headers(basic_auth="{}:{}".format(*auth))
url = httpbin_both.url + "/basic-auth/user/passwd"
with vcr.use_cassette(str(tmpdir.join("auth-failed.yaml"))) as cass:
# Ensure that this is empty to begin with
assert_cassette_empty(cass)
one = verify_pool_mgr.request("GET", url, headers=headers)
two = verify_pool_mgr.request("GET", url, headers=headers)
assert one.data == two.data
assert one.status == two.status == 401
def test_post(tmpdir, httpbin_both, verify_pool_mgr):
"""Ensure that we can post and cache the results"""
data = {"key1": "value1", "key2": "value2"}
url = httpbin_both.url + "/post"
with vcr.use_cassette(str(tmpdir.join("verify_pool_mgr.yaml"))):
req1 = verify_pool_mgr.request("POST", url, data).data
with vcr.use_cassette(str(tmpdir.join("verify_pool_mgr.yaml"))):
req2 = verify_pool_mgr.request("POST", url, data).data
assert req1 == req2
@pytest.mark.online
def test_redirects(tmpdir, verify_pool_mgr, httpbin):
"""Ensure that we can handle redirects"""
url = httpbin.url + "/redirect/1"
with vcr.use_cassette(str(tmpdir.join("verify_pool_mgr.yaml"))):
content = verify_pool_mgr.request("GET", url).data
with vcr.use_cassette(str(tmpdir.join("verify_pool_mgr.yaml"))) as cass:
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
def test_cross_scheme(tmpdir, httpbin, httpbin_secure, verify_pool_mgr):
"""Ensure that requests between schemes are treated separately"""
# First fetch a url under http, and then again under https and then
# ensure that we haven't served anything out of cache, and we have two
# requests / response pairs in the cassette
with vcr.use_cassette(str(tmpdir.join("cross_scheme.yaml"))) as cass:
verify_pool_mgr.request("GET", httpbin_secure.url)
verify_pool_mgr.request("GET", httpbin.url)
assert cass.play_count == 0
assert len(cass) == 2
def test_gzip(tmpdir, httpbin_both, verify_pool_mgr):
"""
Ensure that requests (actually urllib3) is able to automatically decompress
the response body
"""
url = httpbin_both.url + "/gzip"
response = verify_pool_mgr.request("GET", url)
with vcr.use_cassette(str(tmpdir.join("gzip.yaml"))):
response = verify_pool_mgr.request("GET", url)
assert_is_json_bytes(response.data)
with vcr.use_cassette(str(tmpdir.join("gzip.yaml"))):
assert_is_json_bytes(response.data)
def test_https_with_cert_validation_disabled(tmpdir, httpbin_secure, pool_mgr):
with vcr.use_cassette(str(tmpdir.join("cert_validation_disabled.yaml"))):
pool_mgr.request("GET", httpbin_secure.url)
def test_urllib3_force_reset():
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 = conn.HTTPConnection
first_cassette_HTTPSConnection = conn.HTTPSConnection
first_cassette_VerifiedHTTPSConnection = conn.VerifiedHTTPSConnection
with force_reset():
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,66 +1,109 @@
import http.client as httplib
import multiprocessing
from xmlrpc.client import ServerProxy
from xmlrpc.server import SimpleXMLRPCServer
import pytest
requests = pytest.importorskip("requests")
import vcr
try:
import httplib
except ImportError:
import http.client as httplib
requests = pytest.importorskip("requests")
def test_domain_redirect():
'''Ensure that redirects across domains are considered unique'''
"""Ensure that redirects across domains are considered unique"""
# In this example, seomoz.org redirects to moz.com, and if those
# requests are considered identical, then we'll be stuck in a redirect
# loop.
url = 'http://seomoz.org/'
with vcr.use_cassette('tests/fixtures/wild/domain_redirect.yaml') as cass:
requests.get(url, headers={'User-Agent': 'vcrpy-test'})
url = "http://seomoz.org/"
with vcr.use_cassette("tests/fixtures/wild/domain_redirect.yaml") as cass:
requests.get(url, headers={"User-Agent": "vcrpy-test"})
# Ensure that we've now served two responses. One for the original
# redirect, and a second for the actual fetch
assert len(cass) == 2
def test_flickr_multipart_upload():
def test_flickr_multipart_upload(httpbin, tmpdir):
"""
The python-flickr-api project does a multipart
upload that confuses vcrpy
"""
def _pretend_to_be_flickr_library():
content_type, body = "text/plain", "HELLO WORLD"
h = httplib.HTTPConnection("httpbin.org")
headers = {
"Content-Type": content_type,
"content-length": str(len(body))
}
h = httplib.HTTPConnection(httpbin.host, httpbin.port)
headers = {"Content-Type": content_type, "content-length": str(len(body))}
h.request("POST", "/post/", headers=headers)
h.send(body)
r = h.getresponse()
data = r.read()
h.close()
with vcr.use_cassette('fixtures/vcr_cassettes/flickr.yaml') as cass:
return data
testfile = str(tmpdir.join("flickr.yml"))
with vcr.use_cassette(testfile) as cass:
_pretend_to_be_flickr_library()
assert len(cass) == 1
with vcr.use_cassette('fixtures/vcr_cassettes/flickr.yaml') as cass:
with vcr.use_cassette(testfile) as cass:
assert len(cass) == 1
_pretend_to_be_flickr_library()
assert cass.play_count == 1
@pytest.mark.online
def test_flickr_should_respond_with_200(tmpdir):
testfile = str(tmpdir.join('flickr.yml'))
testfile = str(tmpdir.join("flickr.yml"))
with vcr.use_cassette(testfile):
r = requests.post("http://api.flickr.com/services/upload")
r = requests.post("https://api.flickr.com/services/upload", verify=False)
assert r.status_code == 200
def test_cookies(tmpdir):
testfile = str(tmpdir.join('cookies.yml'))
with vcr.use_cassette(testfile):
s = requests.Session()
r1 = s.get("http://httpbin.org/cookies/set?k1=v1&k2=v2")
r2 = s.get("http://httpbin.org/cookies")
assert len(r2.json()['cookies']) == 2
def test_cookies(tmpdir, httpbin):
testfile = str(tmpdir.join("cookies.yml"))
with vcr.use_cassette(testfile), requests.Session() as s:
s.get(httpbin.url + "/cookies/set?k1=v1&k2=v2")
assert s.cookies.keys() == ["k1", "k2"]
r2 = s.get(httpbin.url + "/cookies")
assert sorted(r2.json()["cookies"].keys()) == ["k1", "k2"]
@pytest.mark.online
def test_amazon_doctype(tmpdir):
# amazon gzips its homepage. For some reason, in requests 2.7, it's not
# getting gunzipped.
with vcr.use_cassette(str(tmpdir.join("amz.yml"))):
r = requests.get("http://www.amazon.com", verify=False)
assert "html" in r.text
def start_rpc_server(q):
httpd = SimpleXMLRPCServer(("127.0.0.1", 0))
httpd.register_function(pow)
q.put("http://{}:{}".format(*httpd.server_address))
httpd.serve_forever()
@pytest.fixture(scope="session")
def rpc_server():
q = multiprocessing.Queue()
proxy_process = multiprocessing.Process(target=start_rpc_server, args=(q,))
try:
proxy_process.start()
yield q.get()
finally:
proxy_process.terminate()
def test_xmlrpclib(tmpdir, rpc_server):
with vcr.use_cassette(str(tmpdir.join("xmlrpcvideo.yaml"))):
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 = ServerProxy(rpc_server, allow_none=True)
second_schema = roundup_server.pow(2, 4)
assert original_schema == second_schema

View File

@@ -1,36 +1,67 @@
import contextlib
import copy
import http.client as httplib
import inspect
import os
from unittest import mock
import pytest
import yaml
import mock
from vcr.cassette import Cassette
from vcr.errors import UnhandledHTTPRequestError
from vcr.patch import force_reset
from vcr.request import Request
from vcr.stubs import VCRHTTPSConnection
def test_cassette_load(tmpdir):
a_file = tmpdir.join('test_cassette.yml')
a_file.write(yaml.dump({'interactions': [
{'request': {'body': '', 'uri': 'foo', 'method': 'GET', 'headers': {}},
'response': 'bar'}
]}))
a_cassette = Cassette.load(str(a_file))
a_file = tmpdir.join("test_cassette.yml")
a_file.write(
yaml.dump(
{
"interactions": [
{
"request": {"body": "", "uri": "foo", "method": "GET", "headers": {}},
"response": "bar",
},
],
},
),
)
a_cassette = Cassette.load(path=str(a_file))
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')
a = Cassette("test")
assert not a.play_count
def test_cassette_append():
a = Cassette('test')
a.append('foo', 'bar')
assert a.requests == ['foo']
assert a.responses == ['bar']
a = Cassette("test")
a.append("foo", "bar")
assert a.requests == ["foo"]
assert a.responses == ["bar"]
def test_cassette_len():
a = Cassette('test')
a.append('foo', 'bar')
a.append('foo2', 'bar2')
a = Cassette("test")
a.append("foo", "bar")
a.append("foo2", "bar2")
assert len(a) == 2
@@ -38,45 +69,369 @@ def _mock_requests_match(request1, request2, matchers):
return request1 == request2
@mock.patch('vcr.cassette.requests_match', _mock_requests_match)
@mock.patch("vcr.cassette.requests_match", _mock_requests_match)
def test_cassette_contains():
a = Cassette('test')
a.append('foo', 'bar')
assert 'foo' in a
a = Cassette("test")
a.append("foo", "bar")
assert "foo" in a
@mock.patch('vcr.cassette.requests_match', _mock_requests_match)
@mock.patch("vcr.cassette.requests_match", _mock_requests_match)
def test_cassette_responses_of():
a = Cassette('test')
a.append('foo', 'bar')
assert a.responses_of('foo') == ['bar']
a = Cassette("test")
a.append("foo", "bar")
assert a.responses_of("foo") == ["bar"]
@mock.patch('vcr.cassette.requests_match', _mock_requests_match)
@mock.patch("vcr.cassette.requests_match", _mock_requests_match)
def test_cassette_get_missing_response():
a = Cassette('test')
a = Cassette("test")
with pytest.raises(UnhandledHTTPRequestError):
a.responses_of('foo')
a.responses_of("foo")
@mock.patch('vcr.cassette.requests_match', _mock_requests_match)
@mock.patch("vcr.cassette.requests_match", _mock_requests_match)
def test_cassette_cant_read_same_request_twice():
a = Cassette('test')
a.append('foo', 'bar')
a.play_response('foo')
a = Cassette("test")
a.append("foo", "bar")
a.play_response("foo")
with pytest.raises(UnhandledHTTPRequestError):
a.play_response('foo')
a.play_response("foo")
def make_get_request():
conn = httplib.HTTPConnection("www.python.org")
conn.request("GET", "/index.html")
return conn.getresponse()
@mock.patch("vcr.cassette.requests_match", return_value=True)
@mock.patch(
"vcr.cassette.FilesystemPersister.load_cassette",
classmethod(lambda *args, **kwargs: (("foo",), (mock.MagicMock(),))),
)
@mock.patch("vcr.cassette.Cassette.can_play_response_for", return_value=True)
@mock.patch("vcr.stubs.VCRHTTPResponse")
def test_function_decorated_with_use_cassette_can_be_invoked_multiple_times(*args):
decorated_function = Cassette.use(path="test")(make_get_request)
for _ in range(4):
decorated_function()
def test_arg_getter_functionality():
arg_getter = mock.Mock(return_value={"path": "test"})
context_decorator = Cassette.use_arg_getter(arg_getter)
with context_decorator as cassette:
assert cassette._path == "test"
arg_getter.return_value = {"path": "other"}
with context_decorator as cassette:
assert cassette._path == "other"
arg_getter.return_value = {"path": "other", "filter_headers": ("header_name",)}
@context_decorator
def function():
pass
with mock.patch.object(Cassette, "load", return_value=mock.MagicMock(inject=False)) as cassette_load:
function()
cassette_load.assert_called_once_with(**arg_getter.return_value)
def test_cassette_not_all_played():
a = Cassette('test')
a.append('foo', 'bar')
a = Cassette("test")
a.append("foo", "bar")
assert not a.all_played
@mock.patch('vcr.cassette.requests_match', _mock_requests_match)
@mock.patch("vcr.cassette.requests_match", _mock_requests_match)
def test_cassette_all_played():
a = Cassette('test')
a.append('foo', 'bar')
a.play_response('foo')
a = Cassette("test")
a.append("foo", "bar")
a.play_response("foo")
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 _ 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")
a.append("foo", "bar")
a.play_response("foo")
assert a.all_played
a.rewind()
assert not a.all_played
def test_before_record_response():
before_record_response = mock.Mock(return_value="mutated")
cassette = Cassette("test", before_record_response=before_record_response)
cassette.append("req", "res")
before_record_response.assert_called_once_with("res")
assert cassette.responses[0] == "mutated"
def assert_get_response_body_is(value):
conn = httplib.HTTPConnection("www.python.org")
conn.request("GET", "/index.html")
assert conn.getresponse().read().decode("utf8") == value
@mock.patch("vcr.cassette.requests_match", _mock_requests_match)
@mock.patch("vcr.cassette.Cassette.can_play_response_for", return_value=True)
@mock.patch("vcr.cassette.Cassette._save", return_value=True)
def test_nesting_cassette_context_managers(*args):
first_response = {
"body": {"string": b"first_response"},
"headers": {},
"status": {"message": "m", "code": 200},
}
second_response = copy.deepcopy(first_response)
second_response["body"]["string"] = b"second_response"
with contextlib.ExitStack() as exit_stack:
first_cassette = exit_stack.enter_context(Cassette.use(path="test"))
exit_stack.enter_context(
mock.patch.object(first_cassette, "play_response", return_value=first_response),
)
assert_get_response_body_is("first_response")
# Make sure a second cassette can supersede the first
with (
Cassette.use(path="test") as second_cassette,
mock.patch.object(second_cassette, "play_response", return_value=second_response),
):
assert_get_response_body_is("second_response")
# Now the first cassette should be back in effect
assert_get_response_body_is("first_response")
def test_nesting_context_managers_by_checking_references_of_http_connection():
original = httplib.HTTPConnection
with Cassette.use(path="test"):
first_cassette_HTTPConnection = httplib.HTTPConnection
with Cassette.use(path="test"):
second_cassette_HTTPConnection = httplib.HTTPConnection
assert second_cassette_HTTPConnection is not first_cassette_HTTPConnection
with Cassette.use(path="test"):
assert httplib.HTTPConnection is not second_cassette_HTTPConnection
with force_reset():
assert httplib.HTTPConnection is original
assert httplib.HTTPConnection is second_cassette_HTTPConnection
assert httplib.HTTPConnection is first_cassette_HTTPConnection
def test_custom_patchers():
class Test:
attribute = None
with Cassette.use(path="custom_patches", custom_patches=((Test, "attribute", VCRHTTPSConnection),)):
assert issubclass(Test.attribute, VCRHTTPSConnection)
assert VCRHTTPSConnection is not Test.attribute
old_attribute = Test.attribute
with Cassette.use(path="custom_patches", custom_patches=((Test, "attribute", VCRHTTPSConnection),)):
assert issubclass(Test.attribute, VCRHTTPSConnection)
assert VCRHTTPSConnection is not Test.attribute
assert Test.attribute is not old_attribute
assert issubclass(Test.attribute, VCRHTTPSConnection)
assert VCRHTTPSConnection is not Test.attribute
assert Test.attribute is old_attribute
def test_decorated_functions_are_reentrant():
info = {"second": False}
original_conn = httplib.HTTPConnection
@Cassette.use(path="whatever", inject=True)
def test_function(cassette):
if info["second"]:
assert httplib.HTTPConnection is not info["first_conn"]
else:
info["first_conn"] = httplib.HTTPConnection
info["second"] = True
test_function()
assert httplib.HTTPConnection is info["first_conn"]
test_function()
assert httplib.HTTPConnection is original_conn
def test_cassette_use_called_without_path_uses_function_to_generate_path():
@Cassette.use(inject=True)
def function_name(cassette):
assert cassette._path == "function_name"
function_name()
def test_path_transformer_with_function_path():
def path_transformer(path):
return os.path.join("a", path)
@Cassette.use(inject=True, path_transformer=path_transformer)
def function_name(cassette):
assert cassette._path == os.path.join("a", "function_name")
function_name()
def test_path_transformer_with_context_manager():
with Cassette.use(path="b", path_transformer=lambda *args: "a") as cassette:
assert cassette._path == "a"
def test_path_transformer_None():
with Cassette.use(path="a", path_transformer=None) as cassette:
assert cassette._path == "a"
def test_func_path_generator():
def generator(function):
return os.path.join(os.path.dirname(inspect.getfile(function)), function.__name__)
@Cassette.use(inject=True, func_path_generator=generator)
def function_name(cassette):
assert cassette._path == os.path.join(os.path.dirname(__file__), "function_name")
function_name()
def test_use_as_decorator_on_coroutine():
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_connection
value = yield 1
assert value == 1
assert httplib.HTTPConnection.cassette is cassette
assert httplib.HTTPConnection is not original_http_connection
value = yield 2
assert value == 2
coroutine = test_function()
value = next(coroutine)
while True:
try:
value = coroutine.send(value)
except StopIteration:
break
def test_use_as_decorator_on_generator():
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_connection
yield 1
assert httplib.HTTPConnection.cassette is cassette
assert httplib.HTTPConnection is not original_http_connection
yield 2
assert list(test_function()) == [1, 2]
@mock.patch("vcr.cassette.get_matchers_results")
def test_find_requests_with_most_matches_one_similar_request(mock_get_matchers_results):
mock_get_matchers_results.side_effect = [
(["method"], [("path", "failed : path"), ("query", "failed : query")]),
(["method", "path"], [("query", "failed : query")]),
([], [("method", "failed : method"), ("path", "failed : path"), ("query", "failed : query")]),
]
cassette = Cassette("test")
for request in range(1, 4):
cassette.append(request, "response")
result = cassette.find_requests_with_most_matches("fake request")
assert result == [(2, ["method", "path"], [("query", "failed : query")])]
@mock.patch("vcr.cassette.get_matchers_results")
def test_find_requests_with_most_matches_no_similar_requests(mock_get_matchers_results):
mock_get_matchers_results.side_effect = [
([], [("path", "failed : path"), ("query", "failed : query")]),
([], [("path", "failed : path"), ("query", "failed : query")]),
([], [("path", "failed : path"), ("query", "failed : query")]),
]
cassette = Cassette("test")
for request in range(1, 4):
cassette.append(request, "response")
result = cassette.find_requests_with_most_matches("fake request")
assert result == []
@mock.patch("vcr.cassette.get_matchers_results")
def test_find_requests_with_most_matches_many_similar_requests(mock_get_matchers_results):
mock_get_matchers_results.side_effect = [
(["method", "path"], [("query", "failed : query")]),
(["method"], [("path", "failed : path"), ("query", "failed : query")]),
(["method", "path"], [("query", "failed : query")]),
]
cassette = Cassette("test")
for request in range(1, 4):
cassette.append(request, "response")
result = cassette.find_requests_with_most_matches("fake request")
assert result == [
(1, ["method", "path"], [("query", "failed : query")]),
(3, ["method", "path"], [("query", "failed : query")]),
]
def test_used_interactions(tmpdir):
interactions = [
{"request": {"body": "", "uri": "foo1", "method": "GET", "headers": {}}, "response": "bar1"},
{"request": {"body": "", "uri": "foo2", "method": "GET", "headers": {}}, "response": "bar2"},
{"request": {"body": "", "uri": "foo3", "method": "GET", "headers": {}}, "response": "bar3"},
]
file = tmpdir.join("test_cassette.yml")
file.write(yaml.dump({"interactions": [interactions[0], interactions[1]]}))
cassette = Cassette.load(path=str(file))
request = Request._from_dict(interactions[1]["request"])
cassette.play_response(request)
assert len(cassette._played_interactions) < len(cassette._old_interactions)
request = Request._from_dict(interactions[2]["request"])
cassette.append(request, interactions[2]["response"])
assert len(cassette._new_interactions()) == 1
used_interactions = cassette._played_interactions + cassette._new_interactions()
assert len(used_interactions) == 2

72
tests/unit/test_errors.py Normal file
View File

@@ -0,0 +1,72 @@
from unittest import mock
import pytest
from vcr import errors
from vcr.cassette import Cassette
@mock.patch("vcr.cassette.Cassette.find_requests_with_most_matches")
@pytest.mark.parametrize(
"most_matches, expected_message",
[
# No request match found
([], "No similar requests, that have not been played, found."),
# One matcher failed
(
[("similar request", ["method", "path"], [("query", "failed : query")])],
"Found 1 similar requests with 1 different matcher(s) :\n"
"\n1 - ('similar request').\n"
"Matchers succeeded : ['method', 'path']\n"
"Matchers failed :\n"
"query - assertion failure :\n"
"failed : query\n",
),
# Multiple failed matchers
(
[("similar request", ["method"], [("query", "failed : query"), ("path", "failed : path")])],
"Found 1 similar requests with 2 different matcher(s) :\n"
"\n1 - ('similar request').\n"
"Matchers succeeded : ['method']\n"
"Matchers failed :\n"
"query - assertion failure :\n"
"failed : query\n"
"path - assertion failure :\n"
"failed : path\n",
),
# Multiple similar requests
(
[
("similar request", ["method"], [("query", "failed : query")]),
("similar request 2", ["method"], [("query", "failed : query 2")]),
],
"Found 2 similar requests with 1 different matcher(s) :\n"
"\n1 - ('similar request').\n"
"Matchers succeeded : ['method']\n"
"Matchers failed :\n"
"query - assertion failure :\n"
"failed : query\n"
"\n2 - ('similar request 2').\n"
"Matchers succeeded : ['method']\n"
"Matchers failed :\n"
"query - assertion failure :\n"
"failed : query 2\n",
),
],
)
def test_CannotOverwriteExistingCassetteException_get_message(
mock_find_requests_with_most_matches,
most_matches,
expected_message,
):
mock_find_requests_with_most_matches.return_value = most_matches
cassette = Cassette("path")
failed_request = "request"
exception_message = errors.CannotOverwriteExistingCassetteException._get_message(cassette, "request")
expected = (
f"Can't overwrite existing cassette ({cassette._path!r}) "
f"in your current record mode ({cassette.record_mode!r}).\n"
f"No match for the request ({failed_request!r}) was found.\n"
f"{expected_message}"
)
assert exception_message == expected

View File

@@ -1,37 +1,351 @@
from vcr.filters import _remove_headers, _remove_query_parameters
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_remove_headers():
headers = {'hello': ['goodbye'], 'secret': ['header']}
request = Request('GET', 'http://google.com', '', headers)
_remove_headers(request, ['secret'])
assert request.headers == {'hello': 'goodbye'}
def test_replace_headers():
# This tests all of:
# 1. keeping a header
# 2. removing a header
# 3. replacing a header
# 4. replacing a header using a callable
# 5. removing a header using a callable
# 6. replacing a header that doesn't exist
headers = {"one": ["keep"], "two": ["lose"], "three": ["change"], "four": ["shout"], "five": ["whisper"]}
request = Request("GET", "http://google.com", "", headers)
replace_headers(
request,
[
("two", None),
("three", "tada"),
("four", lambda key, value, request: value.upper()),
("five", lambda key, value, request: None),
("six", "doesntexist"),
],
)
assert request.headers == {"one": "keep", "three": "tada", "four": "SHOUT"}
def test_remove_headers_empty():
headers = {'hello': 'goodbye', 'secret': 'header'}
request = Request('GET', 'http://google.com', '', headers)
_remove_headers(request, [])
def test_replace_headers_empty():
headers = {"hello": "goodbye", "secret": "header"}
request = Request("GET", "http://google.com", "", headers)
replace_headers(request, [])
assert request.headers == headers
def test_remove_query_parameters():
uri = 'http://g.com/?q=cowboys&w=1'
request = Request('GET', uri, '', {})
_remove_query_parameters(request, ['w'])
assert request.uri == 'http://g.com/?q=cowboys'
def test_replace_headers_callable():
# This goes beyond test_replace_headers() to ensure that the callable
# receives the expected arguments.
headers = {"hey": "there"}
request = Request("GET", "http://google.com", "", headers)
callme = mock.Mock(return_value="ho")
replace_headers(request, [("hey", callme)])
assert request.headers == {"hey": "ho"}
assert callme.call_args == ((), {"request": request, "key": "hey", "value": "there"})
def test_remove_headers():
# Test the backward-compatible API wrapper.
headers = {"hello": ["goodbye"], "secret": ["header"]}
request = Request("GET", "http://google.com", "", headers)
remove_headers(request, ["secret"])
assert request.headers == {"hello": "goodbye"}
def test_replace_query_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
uri = "http://g.com/?one=keep&two=lose&three=change&four=shout&five=whisper"
request = Request("GET", uri, "", {})
replace_query_parameters(
request,
[
("two", None),
("three", "tada"),
("four", lambda key, value, request: value.upper()),
("five", lambda key, value, request: None),
("six", "doesntexist"),
],
)
assert request.query == [("four", "SHOUT"), ("one", "keep"), ("three", "tada")]
def test_remove_all_query_parameters():
uri = 'http://g.com/?q=cowboys&w=1'
request = Request('GET', uri, '', {})
_remove_query_parameters(request, ['w', 'q'])
assert request.uri == 'http://g.com/'
uri = "http://g.com/?q=cowboys&w=1"
request = Request("GET", uri, "", {})
replace_query_parameters(request, [("w", None), ("q", None)])
assert request.uri == "http://g.com/"
def test_remove_nonexistent_query_parameters():
uri = 'http://g.com/'
request = Request('GET', uri, '', {})
_remove_query_parameters(request, ['w', 'q'])
assert request.uri == 'http://g.com/'
def test_replace_query_parameters_callable():
# This goes beyond test_replace_query_parameters() to ensure that the
# callable receives the expected arguments.
uri = "http://g.com/?hey=there"
request = Request("GET", uri, "", {})
callme = mock.Mock(return_value="ho")
replace_query_parameters(request, [("hey", callme)])
assert request.uri == "http://g.com/?hey=ho"
assert callme.call_args == ((), {"request": request, "key": "hey", "value": "there"})
def test_remove_query_parameters():
# Test the backward-compatible API wrapper.
uri = "http://g.com/?q=cowboys&w=1"
request = Request("GET", uri, "", {})
remove_query_parameters(request, ["w"])
assert request.uri == "http://g.com/?q=cowboys"
def test_replace_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 = b"one=keep&two=lose&three=change&four=shout&five=whisper"
request = Request("POST", "http://google.com", body, {})
replace_post_data_parameters(
request,
[
("two", None),
("three", "tada"),
("four", lambda key, value, request: value.upper()),
("five", lambda key, value, request: None),
("six", "doesntexist"),
],
)
assert request.body == b"one=keep&three=tada&four=SHOUT"
def test_replace_post_data_parameters_empty_body():
# This test ensures replace_post_data_parameters doesn't throw exception when body is empty.
body = None
request = Request("POST", "http://google.com", body, {})
replace_post_data_parameters(
request,
[
("two", None),
("three", "tada"),
("four", lambda key, value, request: value.upper()),
("five", lambda key, value, request: None),
("six", "doesntexist"),
],
)
assert request.body is None
def test_remove_post_data_parameters():
# Test the backward-compatible API wrapper.
body = b"id=secret&foo=bar"
request = Request("POST", "http://google.com", body, {})
remove_post_data_parameters(request, ["id"])
assert request.body == b"foo=bar"
def test_preserve_multiple_post_data_parameters():
body = b"id=secret&foo=bar&foo=baz"
request = Request("POST", "http://google.com", body, {})
replace_post_data_parameters(request, [("id", None)])
assert request.body == b"foo=bar&foo=baz"
def test_remove_all_post_data_parameters():
body = b"id=secret&foo=bar"
request = Request("POST", "http://google.com", body, {})
replace_post_data_parameters(request, [("id", None), ("foo", None)])
assert request.body == b""
def test_replace_json_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 = b'{"one": "keep", "two": "lose", "three": "change", "four": "shout", "five": "whisper"}'
request = Request("POST", "http://google.com", body, {})
request.headers["Content-Type"] = "application/json"
replace_post_data_parameters(
request,
[
("two", None),
("three", "tada"),
("four", lambda key, value, request: value.upper()),
("five", lambda key, value, request: None),
("six", "doesntexist"),
],
)
request_data = json.loads(request.body)
expected_data = json.loads('{"one": "keep", "three": "tada", "four": "SHOUT"}')
assert request_data == expected_data
def test_remove_json_post_data_parameters():
# Test the backward-compatible API wrapper.
body = b'{"id": "secret", "foo": "bar", "baz": "qux"}'
request = Request("POST", "http://google.com", body, {})
request.headers["Content-Type"] = "application/json"
remove_post_data_parameters(request, ["id"])
request_body_json = json.loads(request.body)
expected_json = json.loads(b'{"foo": "bar", "baz": "qux"}')
assert request_body_json == expected_json
def test_remove_all_json_post_data_parameters():
body = b'{"id": "secret", "foo": "bar"}'
request = Request("POST", "http://google.com", body, {})
request.headers["Content-Type"] = "application/json"
replace_post_data_parameters(request, [("id", None), ("foo", None)])
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},
"headers": {
"content-length": ["10806"],
"date": ["Fri, 24 Oct 2014 18:35:37 GMT"],
"content-type": ["text/html; charset=utf-8"],
},
"body": {"string": b""},
}
assert decode_response(recorded_response) == recorded_response
def test_decode_response_deflate():
body = b"deflate message"
deflate_response = {
"body": {"string": zlib.compress(body)},
"headers": {
"access-control-allow-credentials": ["true"],
"access-control-allow-origin": ["*"],
"connection": ["keep-alive"],
"content-encoding": ["deflate"],
"content-length": ["177"],
"content-type": ["application/json"],
"date": ["Wed, 02 Dec 2015 19:44:32 GMT"],
"server": ["nginx"],
},
"status": {"code": 200, "message": "OK"},
}
decoded_response = decode_response(deflate_response)
assert decoded_response["body"]["string"] == body
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"
buf = BytesIO()
f = gzip.GzipFile("a", fileobj=buf, mode="wb")
f.write(body)
f.close()
compressed_body = buf.getvalue()
buf.close()
gzip_response = {
"body": {"string": compressed_body},
"headers": {
"access-control-allow-credentials": ["true"],
"access-control-allow-origin": ["*"],
"connection": ["keep-alive"],
"content-encoding": ["gzip"],
"content-length": ["177"],
"content-type": ["application/json"],
"date": ["Wed, 02 Dec 2015 19:44:32 GMT"],
"server": ["nginx"],
},
"status": {"code": 200, "message": "OK"},
}
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,19 +1,18 @@
import pytest
from vcr.serializers.jsonserializer import serialize
from vcr.request import Request
from vcr.serializers.jsonserializer import serialize
def test_serialize_binary():
request = Request(
method='GET',
uri='http://localhost/',
body='',
headers={},
)
cassette = {'requests': [request], 'responses': [{'body': b'\x8c'}]}
request = Request(method="GET", uri="http://localhost/", body="", headers={})
cassette = {"requests": [request], "responses": [{"body": b"\x8c"}]}
with pytest.raises(Exception) as e:
serialize(cassette)
assert e.message == "Error serializing cassette to JSON. Does this \
assert (
e.message
== "Error serializing cassette to JSON. Does this \
HTTP interaction contain binary data? If so, use a different \
serializer (like the yaml serializer) for this request"
)

View File

@@ -1,56 +1,339 @@
import itertools
from unittest import mock
from vcr import matchers
from vcr import request
import pytest
from vcr import matchers, request
# the dict contains requests with corresponding to its key difference
# with 'base' request.
REQUESTS = {
'base': request.Request('GET', 'http://host.com/p?a=b', '', {}),
'method': request.Request('POST', 'http://host.com/p?a=b', '', {}),
'scheme': request.Request('GET', 'https://host.com:80/p?a=b', '', {}),
'host': request.Request('GET', 'http://another-host.com/p?a=b', '', {}),
'port': request.Request('GET', 'http://host.com:90/p?a=b', '', {}),
'path': request.Request('GET', 'http://host.com/x?a=b', '', {}),
'query': request.Request('GET', 'http://host.com/p?c=d', '', {}),
"base": request.Request("GET", "http://host.com/p?a=b", "", {}),
"method": request.Request("POST", "http://host.com/p?a=b", "", {}),
"scheme": request.Request("GET", "https://host.com:80/p?a=b", "", {}),
"host": request.Request("GET", "http://another-host.com/p?a=b", "", {}),
"port": request.Request("GET", "http://host.com:90/p?a=b", "", {}),
"path": request.Request("GET", "http://host.com/x?a=b", "", {}),
"query": request.Request("GET", "http://host.com/p?c=d", "", {}),
}
def assert_matcher(matcher_name):
matcher = getattr(matchers, matcher_name)
for k1, k2 in itertools.permutations(REQUESTS, 2):
matched = matcher(REQUESTS[k1], REQUESTS[k2])
if matcher_name in set((k1, k2)):
assert not matched
expecting_assertion_error = matcher_name in {k1, k2}
if expecting_assertion_error:
with pytest.raises(AssertionError):
matcher(REQUESTS[k1], REQUESTS[k2])
else:
assert matched
assert matcher(REQUESTS[k1], REQUESTS[k2]) is None
def test_uri_matcher():
for k1, k2 in itertools.permutations(REQUESTS, 2):
matched = matchers.uri(REQUESTS[k1], REQUESTS[k2])
if set((k1, k2)) != set(('base', 'method')):
assert not matched
expecting_assertion_error = {k1, k2} != {"base", "method"}
if expecting_assertion_error:
with pytest.raises(AssertionError):
matchers.uri(REQUESTS[k1], REQUESTS[k2])
else:
assert matched
assert matchers.uri(REQUESTS[k1], REQUESTS[k2]) is None
req1_body = (
b"<?xml version='1.0'?><methodCall><methodName>test</methodName>"
b"<params><param><value><array><data><value><struct>"
b"<member><name>a</name><value><string>1</string></value></member>"
b"<member><name>b</name><value><string>2</string></value></member>"
b"</struct></value></data></array></value></param></params></methodCall>"
)
req2_body = (
b"<?xml version='1.0'?><methodCall><methodName>test</methodName>"
b"<params><param><value><array><data><value><struct>"
b"<member><name>b</name><value><string>2</string></value></member>"
b"<member><name>a</name><value><string>1</string></value></member>"
b"</struct></value></data></array></value></param></params></methodCall>"
)
boto3_bytes_headers = {
"X-Amz-Content-SHA256": b"UNSIGNED-PAYLOAD",
"Cache-Control": b"max-age=31536000, public",
"X-Amz-Date": b"20191102T143910Z",
"User-Agent": b"Boto3/1.9.102 Python/3.5.3 Linux/4.15.0-54-generic Botocore/1.12.253 Resource",
"Content-MD5": b"GQqjEXsRqrPyxfTl99nkAg==",
"Content-Type": b"text/plain",
"Expect": b"100-continue",
"Content-Length": "21",
}
chunked_headers = {
"Transfer-Encoding": "chunked",
}
@pytest.mark.parametrize(
"r1, r2",
[
(
request.Request("POST", "http://host.com/", "123", {}),
request.Request("POST", "http://another-host.com/", "123", {"Some-Header": "value"}),
),
(
request.Request(
"POST",
"http://host.com/",
"a=1&b=2",
{"Content-Type": "application/x-www-form-urlencoded"},
),
request.Request(
"POST",
"http://host.com/",
"b=2&a=1",
{"Content-Type": "application/x-www-form-urlencoded"},
),
),
(
request.Request("POST", "http://host.com/", "123", {}),
request.Request("POST", "http://another-host.com/", "123", {"Some-Header": "value"}),
),
(
request.Request(
"POST",
"http://host.com/",
"a=1&b=2",
{"Content-Type": "application/x-www-form-urlencoded"},
),
request.Request(
"POST",
"http://host.com/",
"b=2&a=1",
{"Content-Type": "application/x-www-form-urlencoded"},
),
),
(
request.Request(
"POST",
"http://host.com/",
'{"a": 1, "b": 2}',
{"Content-Type": "application/json"},
),
request.Request(
"POST",
"http://host.com/",
'{"b": 2, "a": 1}',
{"content-type": "application/json"},
),
),
(
request.Request(
"POST",
"http://host.com/",
req1_body,
{"User-Agent": "xmlrpclib", "Content-Type": "text/xml"},
),
request.Request(
"POST",
"http://host.com/",
req2_body,
{"user-agent": "somexmlrpc", "content-type": "text/xml"},
),
),
(
request.Request(
"POST",
"http://host.com/",
'{"a": 1, "b": 2}',
{"Content-Type": "application/json"},
),
request.Request(
"POST",
"http://host.com/",
'{"b": 2, "a": 1}',
{"content-type": "application/json"},
),
),
(
# special case for boto3 bytes headers
request.Request("POST", "http://aws.custom.com/", b"123", boto3_bytes_headers),
request.Request("POST", "http://aws.custom.com/", b"123", boto3_bytes_headers),
),
(
# chunked transfer encoding: decoded bytes versus encoded bytes
request.Request("POST", "scheme1://host1.test/", b"123456789_123456", chunked_headers),
request.Request(
"GET",
"scheme2://host2.test/",
b"10\r\n123456789_123456\r\n0\r\n\r\n",
chunked_headers,
),
),
(
# chunked transfer encoding: bytes iterator versus string iterator
request.Request(
"POST",
"scheme1://host1.test/",
iter([b"123456789_", b"123456"]),
chunked_headers,
),
request.Request("GET", "scheme2://host2.test/", iter(["123456789_", "123456"]), chunked_headers),
),
(
# chunked transfer encoding: bytes iterator versus single byte iterator
request.Request(
"POST",
"scheme1://host1.test/",
iter([b"123456789_", b"123456"]),
chunked_headers,
),
request.Request("GET", "scheme2://host2.test/", iter(b"123456789_123456"), chunked_headers),
),
],
)
def test_body_matcher_does_match(r1, r2):
assert matchers.body(r1, r2) is None
@pytest.mark.parametrize(
"r1, r2",
[
(
request.Request("POST", "http://host.com/", '{"a": 1, "b": 2}', {}),
request.Request("POST", "http://host.com/", '{"b": 2, "a": 1}', {}),
),
(
request.Request(
"POST",
"http://host.com/",
'{"a": 1, "b": 3}',
{"Content-Type": "application/json"},
),
request.Request(
"POST",
"http://host.com/",
'{"b": 2, "a": 1}',
{"content-type": "application/json"},
),
),
(
request.Request("POST", "http://host.com/", req1_body, {"Content-Type": "text/xml"}),
request.Request("POST", "http://host.com/", req2_body, {"content-type": "text/xml"}),
),
],
)
def test_body_match_does_not_match(r1, r2):
with pytest.raises(AssertionError):
matchers.body(r1, r2)
def test_query_matcher():
req1 = request.Request('GET', 'http://host.com/?a=b&c=d', '', {})
req2 = request.Request('GET', 'http://host.com/?c=d&a=b', '', {})
assert matchers.query(req1, req2)
req1 = request.Request("GET", "http://host.com/?a=b&c=d", "", {})
req2 = request.Request("GET", "http://host.com/?c=d&a=b", "", {})
assert matchers.query(req1, req2) is None
req1 = request.Request('GET', 'http://host.com/?a=b&a=b&c=d', '', {})
req2 = request.Request('GET', 'http://host.com/?a=b&c=d&a=b', '', {})
req3 = request.Request('GET', 'http://host.com/?c=d&a=b&a=b', '', {})
assert matchers.query(req1, req2)
assert matchers.query(req1, req3)
req1 = request.Request("GET", "http://host.com/?a=b&a=b&c=d", "", {})
req2 = request.Request("GET", "http://host.com/?a=b&c=d&a=b", "", {})
req3 = request.Request("GET", "http://host.com/?c=d&a=b&a=b", "", {})
assert matchers.query(req1, req2) is None
assert matchers.query(req1, req3) is None
def test_metchers():
assert_matcher('method')
assert_matcher('scheme')
assert_matcher('host')
assert_matcher('port')
assert_matcher('path')
assert_matcher('query')
def test_matchers():
assert_matcher("method")
assert_matcher("scheme")
assert_matcher("host")
assert_matcher("port")
assert_matcher("path")
assert_matcher("query")
def test_evaluate_matcher_does_match():
def bool_matcher(r1, r2):
return True
def assertion_matcher(r1, r2):
assert 1 == 1
r1, r2 = None, None
for matcher in [bool_matcher, assertion_matcher]:
match, assertion_msg = matchers._evaluate_matcher(matcher, r1, r2)
assert match is True
assert assertion_msg is None
def test_evaluate_matcher_does_not_match():
def bool_matcher(r1, r2):
return False
def assertion_matcher(r1, r2):
# This is like the "assert" statement preventing pytest to recompile it
raise AssertionError()
r1, r2 = None, None
for matcher in [bool_matcher, assertion_matcher]:
match, assertion_msg = matchers._evaluate_matcher(matcher, r1, r2)
assert match is False
assert not assertion_msg
def test_evaluate_matcher_does_not_match_with_assert_message():
def assertion_matcher(r1, r2):
# This is like the "assert" statement preventing pytest to recompile it
raise AssertionError("Failing matcher")
r1, r2 = None, None
match, assertion_msg = matchers._evaluate_matcher(assertion_matcher, r1, r2)
assert match is False
assert assertion_msg == "Failing matcher"
def test_get_assertion_message():
assert matchers.get_assertion_message(None) is None
assert matchers.get_assertion_message("") == ""
def test_get_assertion_message_with_details():
assertion_msg = "q1=1 != q2=1"
expected = assertion_msg
assert matchers.get_assertion_message(assertion_msg) == expected
@pytest.mark.parametrize(
"r1, r2, expected_successes, expected_failures",
[
(
request.Request("GET", "http://host.com/p?a=b", "", {}),
request.Request("GET", "http://host.com/p?a=b", "", {}),
["method", "path"],
[],
),
(
request.Request("GET", "http://host.com/p?a=b", "", {}),
request.Request("POST", "http://host.com/p?a=b", "", {}),
["path"],
["method"],
),
(
request.Request("GET", "http://host.com/p?a=b", "", {}),
request.Request("POST", "http://host.com/path?a=b", "", {}),
[],
["method", "path"],
),
],
)
def test_get_matchers_results(r1, r2, expected_successes, expected_failures):
successes, failures = matchers.get_matchers_results(r1, r2, [matchers.method, matchers.path])
assert successes == expected_successes
assert len(failures) == len(expected_failures)
for i, expected_failure in enumerate(expected_failures):
assert failures[i][0] == expected_failure
assert failures[i][1] is not None
@mock.patch("vcr.matchers.get_matchers_results")
@pytest.mark.parametrize(
"successes, failures, expected_match",
[(["method", "path"], [], True), (["method"], ["path"], False), ([], ["method", "path"], False)],
)
def test_requests_match(mock_get_matchers_results, successes, failures, expected_match):
mock_get_matchers_results.return_value = (successes, failures)
r1 = request.Request("GET", "http://host.com/p?a=b", "", {})
r2 = request.Request("GET", "http://host.com/p?a=b", "", {})
match = matchers.requests_match(r1, r2, [matchers.method, matchers.path])
assert match is expected_match

View File

@@ -1,41 +1,48 @@
import filecmp
import json
import shutil
import yaml
import vcr.migration
# Use the libYAML versions if possible
try:
from yaml import CLoader as Loader
except ImportError:
from yaml import Loader
def test_try_migrate_with_json(tmpdir):
cassette = tmpdir.join('cassette.json').strpath
shutil.copy('tests/fixtures/migration/old_cassette.json', cassette)
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
def test_try_migrate_with_yaml(tmpdir):
cassette = tmpdir.join('cassette.yaml').strpath
shutil.copy('tests/fixtures/migration/old_cassette.yaml', cassette)
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:
expected_yaml = yaml.load(f)
with open(cassette, 'r') as f:
actual_yaml = yaml.load(f)
with open("tests/fixtures/migration/new_cassette.yaml") as f:
expected_yaml = yaml.load(f, Loader=Loader)
with open(cassette) as f:
actual_yaml = yaml.load(f, Loader=Loader)
assert actual_yaml == expected_yaml
def test_try_migrate_with_invalid_or_new_cassettes(tmpdir):
cassette = tmpdir.join('cassette').strpath
cassette = tmpdir.join("cassette").strpath
files = [
'tests/fixtures/migration/not_cassette.txt',
'tests/fixtures/migration/new_cassette.yaml',
'tests/fixtures/migration/new_cassette.json',
"tests/fixtures/migration/not_cassette.txt",
"tests/fixtures/migration/new_cassette.yaml",
"tests/fixtures/migration/new_cassette.json",
]
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,24 +1,30 @@
import pytest
import vcr.persist
from vcr.persisters.filesystem import FilesystemPersister
from vcr.serializers import jsonserializer, yamlserializer
@pytest.mark.parametrize("cassette_path, serializer", [
('tests/fixtures/migration/old_cassette.json', jsonserializer),
('tests/fixtures/migration/old_cassette.yaml', yamlserializer),
])
@pytest.mark.parametrize(
"cassette_path, serializer",
[
("tests/fixtures/migration/old_cassette.json", jsonserializer),
("tests/fixtures/migration/old_cassette.yaml", yamlserializer),
],
)
def test_load_cassette_with_old_cassettes(cassette_path, serializer):
with pytest.raises(ValueError) as excinfo:
vcr.persist.load_cassette(cassette_path, serializer)
FilesystemPersister.load_cassette(cassette_path, serializer)
assert "run the migration script" in excinfo.exconly()
@pytest.mark.parametrize("cassette_path, serializer", [
('tests/fixtures/migration/not_cassette.txt', jsonserializer),
('tests/fixtures/migration/not_cassette.txt', yamlserializer),
])
@pytest.mark.parametrize(
"cassette_path, serializer",
[
("tests/fixtures/migration/not_cassette.txt", jsonserializer),
("tests/fixtures/migration/not_cassette.txt", yamlserializer),
],
)
def test_load_cassette_with_invalid_cassettes(cassette_path, serializer):
with pytest.raises(Exception) as excinfo:
vcr.persist.load_cassette(cassette_path, serializer)
FilesystemPersister.load_cassette(cassette_path, serializer)
assert "run the migration script" not in excinfo.exconly()

View File

@@ -1,38 +1,85 @@
import pytest
from vcr.request import Request
from vcr.request import HeadersDict, Request
def test_str():
req = Request('GET', 'http://www.google.com/', '', {})
str(req) == '<Request (GET) http://www.google.com/>'
@pytest.mark.parametrize(
"method, uri, expected_str",
[
("GET", "http://www.google.com/", "<Request (GET) http://www.google.com/>"),
("OPTIONS", "*", "<Request (OPTIONS) *>"),
("CONNECT", "host.some.where:1234", "<Request (CONNECT) host.some.where:1234>"),
],
)
def test_str(method, uri, expected_str):
assert str(Request(method, uri, "", {})) == expected_str
def test_headers():
headers = {'X-Header1': ['h1'], 'X-Header2': 'h2'}
req = Request('GET', 'http://go.com/', '', headers)
assert req.headers == {'X-Header1': 'h1', 'X-Header2': 'h2'}
req.add_header('X-Header1', 'h11')
assert req.headers == {'X-Header1': 'h11', 'X-Header2': 'h2'}
headers = {"X-Header1": ["h1"], "X-Header2": "h2"}
req = Request("GET", "http://go.com/", "", headers)
assert req.headers == {"X-Header1": "h1", "X-Header2": "h2"}
req.headers["X-Header1"] = "h11"
assert req.headers == {"X-Header1": "h11", "X-Header2": "h2"}
@pytest.mark.parametrize("uri, expected_port", [
('http://go.com/', 80),
('http://go.com:80/', 80),
('http://go.com:3000/', 3000),
('https://go.com/', 433),
('https://go.com:433/', 433),
('https://go.com:3000/', 3000),
])
def test_add_header_deprecated():
req = Request("GET", "http://go.com/", "", {})
pytest.deprecated_call(req.add_header, "foo", "bar")
assert req.headers == {"foo": "bar"}
@pytest.mark.parametrize(
"uri, expected_port",
[
("http://go.com/", 80),
("http://go.com:80/", 80),
("http://go.com:3000/", 3000),
("https://go.com/", 443),
("https://go.com:443/", 443),
("https://go.com:3000/", 3000),
("*", None),
],
)
def test_port(uri, expected_port):
req = Request('GET', uri, '', {})
req = Request("GET", uri, "", {})
assert req.port == expected_port
def test_uri():
req = Request('GET', 'http://go.com/', '', {})
assert req.uri == 'http://go.com/'
@pytest.mark.parametrize(
"method, uri",
[
("GET", "http://go.com/"),
("GET", "http://go.com:80/"),
("CONNECT", "localhost:1234"),
("OPTIONS", "*"),
],
)
def test_uri(method, uri):
assert Request(method, uri, "", {}).uri == uri
req = Request('GET', 'http://go.com:80/', '', {})
assert req.uri == 'http://go.com:80/'
def test_HeadersDict():
# Simple test of CaseInsensitiveDict
h = HeadersDict()
assert h == {}
h["Content-Type"] = "application/json"
assert h == {"Content-Type": "application/json"}
assert h["content-type"] == "application/json"
assert h["CONTENT-TYPE"] == "application/json"
# Test feature of HeadersDict: devolve list to first element
h = HeadersDict()
assert h == {}
h["x"] = ["foo", "bar"]
assert h == {"x": "foo"}
# Test feature of HeadersDict: preserve original key case
h = HeadersDict()
assert h == {}
h["Content-Type"] = "application/json"
assert h == {"Content-Type": "application/json"}
h["content-type"] = "text/plain"
assert h == {"Content-Type": "text/plain"}
h["CONtent-tyPE"] = "whoa"
assert h == {"Content-Type": "whoa"}

View File

@@ -0,0 +1,98 @@
import io
from vcr.stubs import VCRHTTPResponse
def test_response_should_have_headers_field():
recorded_response = {
"status": {"message": "OK", "code": 200},
"headers": {
"content-length": ["0"],
"server": ["gunicorn/18.0"],
"connection": ["Close"],
"access-control-allow-credentials": ["true"],
"date": ["Fri, 24 Oct 2014 18:35:37 GMT"],
"access-control-allow-origin": ["*"],
"content-type": ["text/html; charset=utf-8"],
},
"body": {"string": b""},
}
response = VCRHTTPResponse(recorded_response)
assert response.headers is not None
def test_response_headers_should_be_equal_to_msg():
recorded_response = {
"status": {"message": b"OK", "code": 200},
"headers": {
"content-length": ["0"],
"server": ["gunicorn/18.0"],
"connection": ["Close"],
"content-type": ["text/html; charset=utf-8"],
},
"body": {"string": b""},
}
response = VCRHTTPResponse(recorded_response)
assert response.headers == response.msg
def test_response_headers_should_have_correct_values():
recorded_response = {
"status": {"message": "OK", "code": 200},
"headers": {
"content-length": ["10806"],
"date": ["Fri, 24 Oct 2014 18:35:37 GMT"],
"content-type": ["text/html; charset=utf-8"],
},
"body": {"string": b""},
}
response = VCRHTTPResponse(recorded_response)
assert response.headers.get("content-length") == "10806"
assert response.headers.get("date") == "Fri, 24 Oct 2014 18:35:37 GMT"
def test_response_parses_correctly_and_fp_attribute_error_is_not_thrown():
"""
Regression test for https://github.com/kevin1024/vcrpy/issues/440
:return:
"""
recorded_response = {
"status": {"message": "OK", "code": 200},
"headers": {
"content-length": ["0"],
"server": ["gunicorn/18.0"],
"connection": ["Close"],
"access-control-allow-credentials": ["true"],
"date": ["Fri, 24 Oct 2014 18:35:37 GMT"],
"access-control-allow-origin": ["*"],
"content-type": ["text/html; charset=utf-8"],
},
"body": {
"string": b"\nPMID- 19416910\nOWN - NLM\nSTAT- MEDLINE\nDA - 20090513\nDCOM- "
b"20090622\nLR - "
b"20141209\nIS - 1091-6490 (Electronic)\nIS - 0027-8424 (Linking)\nVI - "
b"106\nIP - "
b"19\nDP - 2009 May 12\nTI - Genetic dissection of histone deacetylase "
b"requirement in "
b"tumor cells.\nPG - 7751-5\nLID - 10.1073/pnas.0903139106 [doi]\nAB - "
b"Histone "
b"deacetylase inhibitors (HDACi) represent a new group of drugs currently\n "
b" being "
b"tested in a wide variety of clinical applications. They are especially\n "
b" effective "
b"in preclinical models of cancer where they show antiproliferative\n "
b"action in many "
b"different types of cancer cells. Recently, the first HDACi was\n "
b"approved for the "
b"treatment of cutaneous T cell lymphomas. Most HDACi currently in\n "
b"clinical ",
},
}
vcr_response = VCRHTTPResponse(recorded_response)
handle = io.TextIOWrapper(vcr_response, encoding="utf-8")
handle = iter(handle)
articles = list(handle)
assert len(articles) > 1

View File

@@ -1,21 +1,117 @@
from unittest import mock
import pytest
from vcr.serialize import deserialize
from vcr.serializers import yamlserializer, jsonserializer
from vcr.request import Request
from vcr.serialize import deserialize, serialize
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 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 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/old_cassette.yaml") as f, pytest.raises(ValueError):
deserialize(f.read(), yamlserializer)
def test_deserialize_new_json_cassette():
with open('tests/fixtures/migration/new_cassette.json', 'r') as f:
def test_deserialize_old_json_cassette():
with open("tests/fixtures/migration/old_cassette.json") as f, pytest.raises(ValueError):
deserialize(f.read(), jsonserializer)
def test_deserialize_new_yaml_cassette():
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") as f:
deserialize(f.read(), jsonserializer)
REQBODY_TEMPLATE = """\
interactions:
- request:
body: {req_body}
headers:
Content-Type: [application/x-www-form-urlencoded]
Host: [httpbin.org]
method: POST
uri: http://httpbin.org/post
response:
body: {{string: ""}}
headers:
content-length: ['0']
content-type: [application/json]
status: {{code: 200, message: OK}}
"""
# A cassette generated under Python 2 stores the request body as a string,
# but the same cassette generated under Python 3 stores it as "!!binary".
# Make sure we accept both forms, regardless of whether we're running under
# Python 2 or 3.
@pytest.mark.parametrize(
"req_body, expect",
[
# Cassette written under Python 2 (pure ASCII body)
("x=5&y=2", b"x=5&y=2"),
# Cassette written under Python 3 (pure ASCII body)
("!!binary |\n eD01Jnk9Mg==", b"x=5&y=2"),
# Request body has non-ASCII chars (x=föo&y=2), encoded in UTF-8.
('!!python/str "x=f\\xF6o&y=2"', b"x=f\xc3\xb6o&y=2"),
("!!binary |\n eD1mw7ZvJnk9Mg==", b"x=f\xc3\xb6o&y=2"),
# Same request body, this time encoded in UTF-16. In this case, we
# write the same YAML file under both Python 2 and 3, so there's only
# one test case here.
(
"!!binary |\n //54AD0AZgD2AG8AJgB5AD0AMgA=",
b"\xff\xfex\x00=\x00f\x00\xf6\x00o\x00&\x00y\x00=\x002\x00",
),
# Same again, this time encoded in ISO-8859-1.
("!!binary |\n eD1m9m8meT0y", b"x=f\xf6o&y=2"),
],
)
def test_deserialize_py2py3_yaml_cassette(tmpdir, req_body, expect):
cfile = tmpdir.join("test_cassette.yaml")
cfile.write(REQBODY_TEMPLATE.format(req_body=req_body))
with open(str(cfile)) as f:
(requests, _) = deserialize(f.read(), yamlserializer)
assert requests[0].body == expect
@mock.patch.object(
jsonserializer.json,
"dumps",
side_effect=UnicodeDecodeError("utf-8", b"unicode error in serialization", 0, 10, "blew up"),
)
def test_serialize_constructs_UnicodeDecodeError(mock_dumps):
with pytest.raises(UnicodeDecodeError):
jsonserializer.serialize({})
def test_serialize_empty_request():
request = Request(method="POST", uri="http://localhost/", body="", headers={})
serialize({"requests": [request], "responses": [{}]}, jsonserializer)
def test_serialize_json_request():
request = Request(method="POST", uri="http://localhost/", body="{'hello': 'world'}", headers={})
serialize({"requests": [request], "responses": [{}]}, jsonserializer)
def test_serialize_binary_request():
msg = "Does this HTTP interaction contain binary data?"
request = Request(method="POST", uri="http://localhost/", body=b"\x8c", headers={})
try:
serialize({"requests": [request], "responses": [{}]}, jsonserializer)
except (UnicodeDecodeError, TypeError) as exc:
assert msg in str(exc)
def test_deserialize_no_body_string():
data = {"body": {"string": None}}
output = compat.convert_to_bytes(data)
assert data == output

75
tests/unit/test_stubs.py Normal file
View File

@@ -0,0 +1,75 @@
import contextlib
import http.client as httplib
from io import BytesIO
from tempfile import NamedTemporaryFile
from unittest import mock
from pytest import mark
from vcr import mode, use_cassette
from vcr.cassette import Cassette
from vcr.stubs import VCRHTTPSConnection
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):
with contextlib.closing(VCRHTTPSConnection("www.google.com")) as vcr_connection:
vcr_connection.cassette = Cassette("test", record_mode=mode.ALL)
vcr_connection.real_connection.connect()
assert vcr_connection.real_connection.sock is not None
def test_body_consumed_once_stream(self, tmpdir, httpbin):
self._test_body_consumed_once(
tmpdir,
httpbin,
BytesIO(b"1234567890"),
BytesIO(b"9876543210"),
BytesIO(b"9876543210"),
)
def test_body_consumed_once_iterator(self, tmpdir, httpbin):
self._test_body_consumed_once(
tmpdir,
httpbin,
iter([b"1234567890"]),
iter([b"9876543210"]),
iter([b"9876543210"]),
)
# data2 and data3 should serve the same data, potentially as iterators
def _test_body_consumed_once(
self,
tmpdir,
httpbin,
data1,
data2,
data3,
):
with NamedTemporaryFile(dir=tmpdir, suffix=".yml") as f:
testpath = f.name
# NOTE: ``use_cassette`` is not okay with the file existing
# already. So we using ``.close()`` to not only
# close but also delete the empty file, before we start.
f.close()
host, port = httpbin.host, httpbin.port
match_on = ["method", "uri", "body"]
with use_cassette(testpath, match_on=match_on):
conn1 = httplib.HTTPConnection(host, port)
conn1.request("POST", "/anything", body=data1)
conn1.getresponse()
conn2 = httplib.HTTPConnection(host, port)
conn2.request("POST", "/anything", body=data2)
conn2.getresponse()
with use_cassette(testpath, match_on=match_on) as cass:
conn3 = httplib.HTTPConnection(host, port)
conn3.request("POST", "/anything", body=data3)
conn3.getresponse()
assert cass.play_counts[0] == 0
assert cass.play_counts[1] == 1

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 {
"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 {
"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"Example Domain" 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"Example Domain" 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

33
tests/unit/test_util.py Normal file
View File

@@ -0,0 +1,33 @@
from io import BytesIO, StringIO
import pytest
from vcr import request
from vcr.util import read_body
@pytest.mark.parametrize(
"input_, expected_output",
[
(BytesIO(b"Stream"), b"Stream"),
(StringIO("Stream"), b"Stream"),
(iter(["StringIter"]), b"StringIter"),
(iter(["String", "Iter"]), b"StringIter"),
(iter([b"BytesIter"]), b"BytesIter"),
(iter([b"Bytes", b"Iter"]), b"BytesIter"),
(iter([70, 111, 111]), b"Foo"),
(iter([]), b""),
("String", b"String"),
(b"Bytes", b"Bytes"),
],
)
def test_read_body(input_, expected_output):
r = request.Request("POST", "http://host.com/", input_, {})
assert read_body(r) == expected_output
def test_unsupported_read_body():
r = request.Request("POST", "http://host.com/", iter([[]]), {})
with pytest.raises(ValueError) as excinfo:
assert read_body(r)
assert excinfo.value.args == ("Body type <class 'list'> not supported",)

390
tests/unit/test_vcr.py Normal file
View File

@@ -0,0 +1,390 @@
import http.client as httplib
import os
from pathlib import Path
from unittest import mock
import pytest
from vcr import VCR, mode, use_cassette
from vcr.patch import _HTTPConnection, force_reset
from vcr.request import Request
from vcr.stubs import VCRHTTPSConnection
def test_vcr_use_cassette():
record_mode = mock.Mock()
test_vcr = VCR(record_mode=record_mode)
with mock.patch(
"vcr.cassette.Cassette.load",
return_value=mock.MagicMock(inject=False),
) as mock_cassette_load:
@test_vcr.use_cassette("test")
def function():
pass
assert mock_cassette_load.call_count == 0
function()
assert mock_cassette_load.call_args[1]["record_mode"] is record_mode
# Make sure that calls to function now use cassettes with the
# new filter_header_settings
test_vcr.record_mode = mock.Mock()
function()
assert mock_cassette_load.call_args[1]["record_mode"] == test_vcr.record_mode
# Ensure that explicitly provided arguments still supersede
# those on the vcr.
new_record_mode = mock.Mock()
with test_vcr.use_cassette("test", record_mode=new_record_mode) as cassette:
assert cassette.record_mode == new_record_mode
def test_vcr_before_record_request_params():
base_path = "http://whatever.test/"
def before_record_cb(request):
if request.path != "/get":
return request
test_vcr = VCR(
filter_headers=("cookie", ("bert", "ernie")),
before_record_request=before_record_cb,
ignore_hosts=("www.test.com",),
ignore_localhost=True,
filter_query_parameters=("foo", ("tom", "jerry")),
filter_post_data_parameters=("posted", ("no", "trespassing")),
)
with test_vcr.use_cassette("test") as cassette:
# Test explicit before_record_cb
request_get = Request("GET", base_path + "get", "", {})
assert cassette.filter_request(request_get) is None
request = Request("GET", base_path + "get2", "", {})
assert cassette.filter_request(request) is not None
# Test filter_query_parameters
request = Request("GET", base_path + "?foo=bar", "", {})
assert cassette.filter_request(request).query == []
request = Request("GET", base_path + "?tom=nobody", "", {})
assert cassette.filter_request(request).query == [("tom", "jerry")]
# Test filter_headers
request = Request(
"GET",
base_path + "?foo=bar",
"",
{"cookie": "test", "other": "fun", "bert": "nobody"},
)
assert cassette.filter_request(request).headers == {"other": "fun", "bert": "ernie"}
# Test ignore_hosts
request = Request("GET", "http://www.test.com?foo=bar", "", {"cookie": "test", "other": "fun"})
assert cassette.filter_request(request) is None
# Test ignore_localhost
request = Request("GET", "http://localhost:8000?foo=bar", "", {"cookie": "test", "other": "fun"})
assert cassette.filter_request(request) is None
with test_vcr.use_cassette("test", before_record_request=None) as cassette:
# Test that before_record can be overwritten in context manager.
assert cassette.filter_request(request_get) is not None
def test_vcr_before_record_response_iterable():
# Regression test for #191
request = Request("GET", "/", "", {})
response = object() # just can't be None
# 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)
with vcr.use_cassette("test") as cassette:
assert mock_filter.call_count == 0
cassette.append(request, response)
assert mock_filter.call_count == 1
# Regression test: iterable before_record_response should work too
mock_filter = mock.Mock()
vcr = VCR(before_record_response=(mock_filter,))
with vcr.use_cassette("test") as cassette:
assert mock_filter.call_count == 0
cassette.append(request, response)
assert mock_filter.call_count == 1
def test_before_record_response_as_filter():
request = Request("GET", "/", "", {})
response = object() # just can't be None
# 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:
cassette.append(request, response)
assert cassette.data == []
assert not cassette.dirty
def test_vcr_path_transformer():
# Regression test for #199
# 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:
assert cassette._path == "test"
# Regression test: path_transformer=None should do the same.
vcr = VCR(path_transformer=None)
with vcr.use_cassette("test") as cassette:
assert cassette._path == "test"
# and it should still work with cassette_library_dir
vcr = VCR(cassette_library_dir="/foo")
with vcr.use_cassette("test") as cassette:
assert os.path.abspath(cassette._path) == os.path.abspath("/foo/test")
@pytest.fixture
def random_fixture():
return 1
@use_cassette("test")
def test_fixtures_with_use_cassette(random_fixture):
# Applying a decorator to a test function that requests features can cause
# problems if the decorator does not preserve the signature of the original
# test function.
# This test ensures that use_cassette preserves the signature of
# the original test function, and thus that use_cassette is
# compatible with py.test fixtures. It is admittedly a bit strange
# because the test would never even run if the relevant feature
# were broken.
pass
def test_custom_patchers():
class Test:
attribute = None
attribute2 = None
test_vcr = VCR(custom_patches=((Test, "attribute", VCRHTTPSConnection),))
with test_vcr.use_cassette("custom_patches"):
assert issubclass(Test.attribute, VCRHTTPSConnection)
assert VCRHTTPSConnection is not Test.attribute
with test_vcr.use_cassette("custom_patches", custom_patches=((Test, "attribute2", VCRHTTPSConnection),)):
assert issubclass(Test.attribute, VCRHTTPSConnection)
assert VCRHTTPSConnection is not Test.attribute
assert Test.attribute is Test.attribute2
def test_inject_cassette():
vcr = VCR(inject_cassette=True)
@vcr.use_cassette("test", record_mode=mode.ONCE)
def with_cassette_injected(cassette):
assert cassette.record_mode == mode.ONCE
@vcr.use_cassette("test", record_mode=mode.ONCE, inject_cassette=False)
def without_cassette_injected():
pass
with_cassette_injected()
without_cassette_injected()
def test_with_current_defaults():
vcr = VCR(inject_cassette=True, record_mode=mode.ONCE)
@vcr.use_cassette("test", with_current_defaults=False)
def changing_defaults(cassette, checks):
checks(cassette)
@vcr.use_cassette("test", with_current_defaults=True)
def current_defaults(cassette, checks):
checks(cassette)
def assert_record_mode_once(cassette):
assert cassette.record_mode == mode.ONCE
def assert_record_mode_all(cassette):
assert cassette.record_mode == mode.ALL
changing_defaults(assert_record_mode_once)
current_defaults(assert_record_mode_once)
vcr.record_mode = "all"
changing_defaults(assert_record_mode_all)
current_defaults(assert_record_mode_once)
def test_cassette_library_dir_with_decoration_and_no_explicit_path():
library_dir = "/library_dir"
vcr = VCR(inject_cassette=True, cassette_library_dir=library_dir)
@vcr.use_cassette()
def function_name(cassette):
assert cassette._path == os.path.join(library_dir, "function_name")
function_name()
def test_cassette_library_dir_with_decoration_and_explicit_path():
library_dir = "/library_dir"
vcr = VCR(inject_cassette=True, cassette_library_dir=library_dir)
@vcr.use_cassette(path="custom_name")
def function_name(cassette):
assert cassette._path == os.path.join(library_dir, "custom_name")
function_name()
def test_cassette_library_dir_with_decoration_and_super_explicit_path():
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"))
def function_name(cassette):
assert cassette._path == os.path.join(library_dir, "custom_name")
function_name()
def test_cassette_library_dir_with_path_transformer():
library_dir = "/library_dir"
vcr = VCR(
inject_cassette=True,
cassette_library_dir=library_dir,
path_transformer=lambda path: path + ".json",
)
@vcr.use_cassette()
def function_name(cassette):
assert cassette._path == os.path.join(library_dir, "function_name.json")
function_name()
def test_use_cassette_with_no_extra_invocation():
vcr = VCR(inject_cassette=True, cassette_library_dir="/")
@vcr.use_cassette
def function_name(cassette):
assert cassette._path == os.path.join("/", "function_name")
function_name()
def test_path_transformer():
vcr = VCR(inject_cassette=True, cassette_library_dir="/", path_transformer=lambda x: x + "_test")
@vcr.use_cassette
def function_name(cassette):
assert cassette._path == os.path.join("/", "function_name_test")
function_name()
def test_cassette_name_generator_defaults_to_using_module_function_defined_in():
vcr = VCR(inject_cassette=True)
@vcr.use_cassette
def function_name(cassette):
assert cassette._path == os.path.join(os.path.dirname(__file__), "function_name")
function_name()
def test_ensure_suffix():
vcr = VCR(inject_cassette=True, path_transformer=VCR.ensure_suffix(".yaml"))
@vcr.use_cassette
def function_name(cassette):
assert cassette._path == os.path.join(os.path.dirname(__file__), "function_name.yaml")
function_name()
def test_additional_matchers():
vcr = VCR(match_on=("uri",), inject_cassette=True)
@vcr.use_cassette
def function_defaults(cassette):
assert set(cassette._match_on) == {vcr.matchers["uri"]}
@vcr.use_cassette(additional_matchers=("body",))
def function_additional(cassette):
assert set(cassette._match_on) == {vcr.matchers["uri"], vcr.matchers["body"]}
function_defaults()
function_additional()
def test_decoration_should_respect_function_return_value():
vcr = VCR()
ret = "a-return-value"
@vcr.use_cassette
def function_with_return():
return ret
assert ret == function_with_return()
class TestVCRClass(VCR().test_case()):
def no_decoration(self):
assert httplib.HTTPConnection == _HTTPConnection
self.test_dynamically_added()
assert httplib.HTTPConnection == _HTTPConnection
def test_one(self):
with force_reset():
self.no_decoration()
with force_reset():
self.test_two()
assert httplib.HTTPConnection != _HTTPConnection
def test_two(self):
assert httplib.HTTPConnection != _HTTPConnection
def test_dynamically_added(self):
assert httplib.HTTPConnection != _HTTPConnection
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
def test_use_cassette_generator_return():
ret_val = object()
vcr = VCR()
@vcr.use_cassette("test")
def gen():
return ret_val
yield
with pytest.raises(StopIteration) as exc_info:
next(gen())
assert exc_info.value.value is ret_val

View File

@@ -0,0 +1,11 @@
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"]
import vcr # noqa: F401
assert len(recwarn) == 0

170
tox.ini
View File

@@ -1,170 +0,0 @@
# Tox (http://tox.testrun.org/) is a tool for running tests
# in multiple virtualenvs. This configuration file will run the
# test suite on all supported python versions. To use it, "pip install tox"
# and then run "tox" from this directory.
[tox]
envlist =
py26,
py27,
py33,
py34,
pypy,
py26requests,
py27requests,
py34requests,
pypyrequests,
py26oldrequests,
py27oldrequests,
py33oldrequests,
py34oldrequests,
pypyoldrequests,
py26httplib2,
py27httplib2,
py33httplib2,
py34httplib2,
pypyhttplib2,
[testenv]
commands =
py.test {posargs}
deps =
mock
pytest
pytest-localserver
PyYAML
[testenv:py26oldrequests]
basepython = python2.6
deps =
mock
pytest
pytest-localserver
PyYAML
requests==1.2.3
[testenv:py27oldrequests]
basepython = python2.7
deps =
mock
pytest
pytest-localserver
PyYAML
requests==1.2.3
[testenv:py33oldrequests]
basepython = python3.3
deps =
mock
pytest
pytest-localserver
PyYAML
requests==1.2.3
[testenv:py34oldrequests]
basepython = python3.4
deps =
mock
pytest
pytest-localserver
PyYAML
requests==1.2.3
[testenv:pypyoldrequests]
basepython = pypy
deps =
mock
pytest
pytest-localserver
PyYAML
requests==1.2.3
[testenv:py26requests]
basepython = python2.6
deps =
mock
pytest
pytest-localserver
PyYAML
requests
[testenv:py27requests]
basepython = python2.7
deps =
mock
pytest
pytest-localserver
PyYAML
requests
[testenv:py33requests]
basepython = python3.4
deps =
mock
pytest
pytest-localserver
PyYAML
requests
[testenv:py34requests]
basepython = python3.4
deps =
mock
pytest
pytest-localserver
PyYAML
requests
[testenv:pypyrequests]
basepython = pypy
deps =
mock
pytest
pytest-localserver
PyYAML
requests
[testenv:py26httplib2]
basepython = python2.6
deps =
mock
pytest
pytest-localserver
PyYAML
httplib2
[testenv:py27httplib2]
basepython = python2.7
deps =
mock
pytest
pytest-localserver
PyYAML
httplib2
[testenv:py33httplib2]
basepython = python3.4
deps =
mock
pytest
pytest-localserver
PyYAML
httplib2
[testenv:py34httplib2]
basepython = python3.4
deps =
mock
pytest
pytest-localserver
PyYAML
httplib2
[testenv:pypyhttplib2]
basepython = pypy
deps =
mock
pytest
pytest-localserver
PyYAML
httplib2

BIN
vcr.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 240 KiB

After

Width:  |  Height:  |  Size: 114 KiB

View File

@@ -1,19 +1,13 @@
import logging
from .config import VCR
from logging import NullHandler
# Set default logging handler to avoid "No handler found" warnings.
import logging
try: # Python 2.7+
from logging import NullHandler
except ImportError:
class NullHandler(logging.Handler):
def emit(self, record):
pass
from .config import VCR
from .record_mode import RecordMode as mode # noqa: F401
__version__ = "8.0.0"
logging.getLogger(__name__).addHandler(NullHandler())
default_vcr = VCR()
def use_cassette(path, **kwargs):
return default_vcr.use_cassette(path, **kwargs)
use_cassette = default_vcr.use_cassette

3
vcr/_handle_coroutine.py Normal file
View File

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

View File

@@ -1,60 +1,206 @@
'''The container for recorded requests and responses'''
import collections
import contextlib
import copy
import inspect
import logging
from inspect import iscoroutinefunction
try:
from collections import Counter
except ImportError:
from .compat.counter import Counter
import wrapt
from contextdecorator import ContextDecorator
# Internal imports
from .patch import install, reset
from .persist import load_cassette, save_cassette
from .filters import filter_request
from .serializers import yamlserializer
from .matchers import requests_match, uri, method
from ._handle_coroutine import handle_coroutine
from .errors import UnhandledHTTPRequestError
from .matchers import get_matchers_results, method, requests_match, uri
from .patch import CassettePatcherBuilder
from .persisters.filesystem import CassetteDecodeError, CassetteNotFoundError, FilesystemPersister
from .record_mode import RecordMode
from .serializers import yamlserializer
from .util import partition_dict
log = logging.getLogger(__name__)
class Cassette(ContextDecorator):
'''A container for recorded requests and responses'''
class CassetteContextDecorator:
"""Context manager/decorator that handles installing the cassette and
removing cassettes.
This class defers the creation of a new cassette instance until
the point at which it is installed by context manager or
decorator. The fact that a new cassette is used with each
application prevents the state of any cassette from interfering
with another.
Instances of this class are NOT reentrant as context managers.
However, functions that are decorated by
``CassetteContextDecorator`` instances ARE reentrant. See the
implementation of ``__call__`` on this class for more details.
There is also a guard against attempts to reenter instances of
this class as a context manager in ``__exit__``.
"""
_non_cassette_arguments = (
"path_transformer",
"func_path_generator",
"record_on_exception",
)
@classmethod
def load(cls, path, **kwargs):
'''Load in the cassette stored at the provided path'''
new_cassette = cls(path, **kwargs)
def from_args(cls, cassette_class, **kwargs):
return cls(cassette_class, lambda: dict(kwargs))
def __init__(self, cls, args_getter):
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:
for patcher in CassettePatcherBuilder(cassette).build():
exit_stack.enter_context(patcher)
log_format = "{action} context for cassette at {path}."
log.debug(log_format.format(action="Entering", path=cassette._path))
yield cassette
log.debug(log_format.format(action="Exiting", path=cassette._path))
def __enter__(self):
# This assertion is here to prevent the dangerous behavior
# that would result from forgetting about a __finish before
# completing it.
# How might this condition be met? Here is an example:
# context_decorator = Cassette.use('whatever')
# with context_decorator:
# with context_decorator:
# pass
assert self.__finish is None, "Cassette already open."
other_kwargs, cassette_kwargs = partition_dict(
lambda key, _: key in self._non_cassette_arguments,
self._args_getter(),
)
if other_kwargs.get("path_transformer"):
transformer = other_kwargs["path_transformer"]
cassette_kwargs["path"] = transformer(cassette_kwargs["path"])
self.__cassette = self.cls.load(**cassette_kwargs)
self.__finish = self._patch_generator(self.__cassette)
return next(self.__finish)
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
@wrapt.decorator
def __call__(self, function, instance, args, kwargs):
# This awkward cloning thing is done to ensure that decorated
# functions are reentrant. This is required for thread
# safety and the correct operation of recursive functions.
args_getter = self._build_args_getter_for_decorator(function)
return type(self)(self.cls, args_getter)._execute_function(function, args, kwargs)
def _execute_function(self, function, args, kwargs):
def handle_function(cassette):
if cassette.inject:
return function(cassette, *args, **kwargs)
else:
return function(*args, **kwargs)
if iscoroutinefunction(function):
return handle_coroutine(vcr=self, fn=handle_function)
if inspect.isgeneratorfunction(function):
return self._handle_generator(fn=handle_function)
return self._handle_function(fn=handle_function)
def _handle_generator(self, fn):
"""Wraps a generator so that we're inside the cassette context for the
duration of the generator.
"""
with self as cassette:
return (yield from fn(cassette))
def _handle_function(self, fn):
with self as cassette:
return fn(cassette)
@staticmethod
def get_function_name(function):
return function.__name__
def _build_args_getter_for_decorator(self, function):
def new_args_getter():
kwargs = self._args_getter()
if "path" not in kwargs:
name_generator = kwargs.get("func_path_generator") or self.get_function_name
path = name_generator(function)
kwargs["path"] = path
return kwargs
return new_args_getter
class Cassette:
"""A container for recorded requests and responses"""
@classmethod
def load(cls, **kwargs):
"""Instantiate and load the cassette stored at the specified path."""
new_cassette = cls(**kwargs)
new_cassette._load()
return new_cassette
def __init__(self,
path,
serializer=yamlserializer,
record_mode='once',
match_on=[uri, method],
filter_headers=[],
filter_query_parameters=[],
before_record=None,
ignore_hosts=[],
ignore_localhost=[],
):
@classmethod
def use_arg_getter(cls, arg_getter):
return CassetteContextDecorator(cls, arg_getter)
@classmethod
def use(cls, **kwargs):
return CassetteContextDecorator.from_args(cls, **kwargs)
def __init__(
self,
path,
serializer=None,
persister=None,
record_mode=RecordMode.ONCE,
match_on=(uri, method),
before_record_request=None,
before_record_response=None,
custom_patches=(),
inject=False,
allow_playback_repeats=False,
drop_unused_requests=False,
):
self._persister = persister or FilesystemPersister
self._path = path
self._serializer = serializer
self._serializer = serializer or yamlserializer
self._match_on = match_on
self._filter_headers = filter_headers
self._filter_query_parameters = filter_query_parameters
self._before_record = before_record
self._ignore_hosts = ignore_hosts
if ignore_localhost:
self._ignore_hosts = list(set(
self._ignore_hosts + ['localhost', '0.0.0.0', '127.0.0.1']
))
self._before_record_request = before_record_request or (lambda x: x)
log.info(self._before_record_request)
self._before_record_response = before_record_response or (lambda x: x)
self.inject = inject
self.record_mode = record_mode
self.custom_patches = custom_patches
self.allow_playback_repeats = allow_playback_repeats
self.drop_unused_requests = drop_unused_requests
# self.data is the list of (req, resp) tuples
self.data = []
self.play_counts = Counter()
self.play_counts = collections.Counter()
self.dirty = False
self.rewound = False
self.record_mode = record_mode
# Subsets of self.data to store old and played interactions
self._old_interactions = []
self._played_interactions = []
@property
def play_count(self):
@@ -62,10 +208,8 @@ class Cassette(ContextDecorator):
@property
def all_played(self):
"""
Returns True if all responses have been played, False otherwise.
"""
return self.play_count == len(self)
"""Returns True if all responses have been played, False otherwise."""
return len(self.play_counts.values()) == len(self)
@property
def requests(self):
@@ -77,120 +221,162 @@ class Cassette(ContextDecorator):
@property
def write_protected(self):
return self.rewound and self.record_mode == 'once' or \
self.record_mode == 'none'
def _filter_request(self, request):
return filter_request(
request=request,
filter_headers=self._filter_headers,
filter_query_parameters=self._filter_query_parameters,
before_record=self._before_record,
ignore_hosts=self._ignore_hosts
)
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'''
request = self._filter_request(request)
"""Add a request, response pair to this cassette"""
request = self._before_record_request(request)
if not request:
return
log.info("Appending request %s and response %s", request, response)
# Deepcopy is here because mutation of `response` will corrupt the
# real response.
response = copy.deepcopy(response)
response = self._before_record_response(response)
if response is None:
return
self.data.append((request, response))
self.dirty = True
def filter_request(self, request):
return self._before_record_request(request)
def _responses(self, request):
"""
internal API, returns an iterator with all responses matching
the request.
"""
request = self._filter_request(request)
if not request:
return
request = self._before_record_request(request)
for index, (stored_request, response) in enumerate(self.data):
if requests_match(request, stored_request, self._match_on):
yield index, response
def can_play_response_for(self, request):
request = self._filter_request(request)
return request and request in self and \
self.record_mode != 'all' and \
self.rewound
request = self._before_record_request(request)
return request and request in self and self.record_mode != RecordMode.ALL and self.rewound
def play_response(self, request):
'''
"""
Get the response corresponding to a request, but only if it
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
self._played_interactions.append((request, response))
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):
'''
"""
Find the responses corresponding to a request.
This function isn't actually used by VCR internally, but is
provided as an external API.
'''
"""
responses = [response for index, response in self._responses(request)]
if responses:
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):
self.play_counts = collections.Counter()
def find_requests_with_most_matches(self, request):
"""
Get the most similar request(s) stored in the cassette
of a given request as a list of tuples like this:
- the request object
- the successful matchers as string
- the failed matchers and the related assertion message with the difference details as strings tuple
This is useful when a request failed to be found,
we can get the similar request(s) in order to know what have changed in the request parts.
"""
best_matches = []
request = self._before_record_request(request)
for _, (stored_request, _) in enumerate(self.data):
successes, fails = get_matchers_results(request, stored_request, self._match_on)
best_matches.append((len(successes), stored_request, successes, fails))
best_matches.sort(key=lambda t: t[0], reverse=True)
# Get the first best matches (multiple if equal matches)
final_best_matches = []
if not best_matches:
return final_best_matches
previous_nb_success = best_matches[0][0]
for best_match in best_matches:
nb_success = best_match[0]
# Do not keep matches that have 0 successes,
# it means that the request is totally different from
# the ones stored in the cassette
if nb_success < 1 or previous_nb_success != nb_success:
break
previous_nb_success = nb_success
final_best_matches.append(best_match[1:])
return final_best_matches
def _new_interactions(self):
"""List of new HTTP interactions (request/response tuples)"""
new_interactions = []
for request, response in self.data:
if all(
not requests_match(request, old_request, self._match_on)
for old_request, _ in self._old_interactions
):
new_interactions.append((request, response))
return new_interactions
def _as_dict(self):
return {"requests": self.requests, "responses": self.responses}
def _build_used_interactions_dict(self):
interactions = self._played_interactions + self._new_interactions()
cassete_dict = {
"requests": [request for request, _ in interactions],
"responses": [response for _, response in interactions],
}
return cassete_dict
def _save(self, force=False):
if self.drop_unused_requests and len(self._played_interactions) < len(self._old_interactions):
cassete_dict = self._build_used_interactions_dict()
force = True
else:
cassete_dict = self._as_dict()
if force or self.dirty:
save_cassette(
self._path,
self._as_dict(),
serializer=self._serializer
)
self._persister.save_cassette(self._path, cassete_dict, serializer=self._serializer)
self.dirty = False
def _load(self):
try:
requests, responses = load_cassette(
self._path,
serializer=self._serializer
)
for request, response in zip(requests, responses):
requests, responses = self._persister.load_cassette(self._path, serializer=self._serializer)
for request, response in zip(requests, responses, strict=False):
self.append(request, response)
self._old_interactions.append((request, response))
self.dirty = False
self.rewound = True
except IOError:
except (CassetteDecodeError, CassetteNotFoundError):
pass
def __str__(self):
return "<Cassette containing {0} 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'''
"""Return the number of request,response pairs stored in here"""
return len(self.data)
def __contains__(self, request):
'''Return whether or not a request has been stored'''
for response in self._responses(request):
return True
"""Return whether or not a request has been stored"""
for index, _ in self._responses(request):
if self.play_counts[index] == 0 or self.allow_playback_repeats:
return True
return False
def __enter__(self):
'''Patch the fetching libraries we know about'''
install(self)
return self
def __exit__(self, typ, value, traceback):
self._save()
reset()

View File

@@ -1,193 +0,0 @@
from operator import itemgetter
from heapq import nlargest
from itertools import repeat, ifilter
# From http://code.activestate.com/recipes/576611-counter-class/
# Backported for python 2.6 support
class Counter(dict):
'''Dict subclass for counting hashable objects. Sometimes called a bag
or multiset. Elements are stored as dictionary keys and their counts
are stored as dictionary values.
>>> Counter('zyzygy')
Counter({'y': 3, 'z': 2, 'g': 1})
'''
def __init__(self, iterable=None, **kwds):
'''Create a new, empty Counter object. And if given, count elements
from an input iterable. Or, initialize the count from another mapping
of elements to their counts.
>>> c = Counter() # a new, empty counter
>>> c = Counter('gallahad') # a new counter from an iterable
>>> c = Counter({'a': 4, 'b': 2}) # a new counter from a mapping
>>> c = Counter(a=4, b=2) # a new counter from keyword args
'''
self.update(iterable, **kwds)
def __missing__(self, key):
return 0
def most_common(self, n=None):
'''List the n most common elements and their counts from the most
common to the least. If n is None, then list all element counts.
>>> Counter('abracadabra').most_common(3)
[('a', 5), ('r', 2), ('b', 2)]
'''
if n is None:
return sorted(self.iteritems(), key=itemgetter(1), reverse=True)
return nlargest(n, self.iteritems(), key=itemgetter(1))
def elements(self):
'''Iterator over elements repeating each as many times as its count.
>>> c = Counter('ABCABC')
>>> sorted(c.elements())
['A', 'A', 'B', 'B', 'C', 'C']
If an element's count has been set to zero or is a negative number,
elements() will ignore it.
'''
for elem, count in self.iteritems():
for _ in repeat(None, count):
yield elem
# Override dict methods where the meaning changes for Counter objects.
@classmethod
def fromkeys(cls, iterable, v=None):
raise NotImplementedError(
'Counter.fromkeys() is undefined. Use Counter(iterable) instead.')
def update(self, iterable=None, **kwds):
'''Like dict.update() but add counts instead of replacing them.
Source can be an iterable, a dictionary, or another Counter instance.
>>> c = Counter('which')
>>> c.update('witch') # add elements from another iterable
>>> d = Counter('watch')
>>> c.update(d) # add elements from another counter
>>> c['h'] # four 'h' in which, witch, and watch
4
'''
if iterable is not None:
if hasattr(iterable, 'iteritems'):
if self:
self_get = self.get
for elem, count in iterable.iteritems():
self[elem] = self_get(elem, 0) + count
else:
dict.update(self, iterable) # fast path when counter is empty
else:
self_get = self.get
for elem in iterable:
self[elem] = self_get(elem, 0) + 1
if kwds:
self.update(kwds)
def copy(self):
'Like dict.copy() but returns a Counter instance instead of a dict.'
return Counter(self)
def __delitem__(self, elem):
'Like dict.__delitem__() but does not raise KeyError for missing values.'
if elem in self:
dict.__delitem__(self, elem)
def __repr__(self):
if not self:
return '%s()' % self.__class__.__name__
items = ', '.join(map('%r: %r'.__mod__, self.most_common()))
return '%s({%s})' % (self.__class__.__name__, items)
# Multiset-style mathematical operations discussed in:
# Knuth TAOCP Volume II section 4.6.3 exercise 19
# and at http://en.wikipedia.org/wiki/Multiset
#
# Outputs guaranteed to only include positive counts.
#
# To strip negative and zero counts, add-in an empty counter:
# c += Counter()
def __add__(self, other):
'''Add counts from two counters.
>>> Counter('abbb') + Counter('bcc')
Counter({'b': 4, 'c': 2, 'a': 1})
'''
if not isinstance(other, Counter):
return NotImplemented
result = Counter()
for elem in set(self) | set(other):
newcount = self[elem] + other[elem]
if newcount > 0:
result[elem] = newcount
return result
def __sub__(self, other):
''' Subtract count, but keep only results with positive counts.
>>> Counter('abbbc') - Counter('bccd')
Counter({'b': 2, 'a': 1})
'''
if not isinstance(other, Counter):
return NotImplemented
result = Counter()
for elem in set(self) | set(other):
newcount = self[elem] - other[elem]
if newcount > 0:
result[elem] = newcount
return result
def __or__(self, other):
'''Union is the maximum of value in either of the input counters.
>>> Counter('abbb') | Counter('bcc')
Counter({'b': 3, 'c': 2, 'a': 1})
'''
if not isinstance(other, Counter):
return NotImplemented
_max = max
result = Counter()
for elem in set(self) | set(other):
newcount = _max(self[elem], other[elem])
if newcount > 0:
result[elem] = newcount
return result
def __and__(self, other):
''' Intersection is the minimum of corresponding counts.
>>> Counter('abbb') & Counter('bcc')
Counter({'b': 1})
'''
if not isinstance(other, Counter):
return NotImplemented
_min = min
result = Counter()
if len(self) < len(other):
self, other = other, self
for elem in ifilter(self.__contains__, other):
newcount = _min(self[elem], other[elem])
if newcount > 0:
result[elem] = newcount
return result
if __name__ == '__main__':
import doctest
print doctest.testmod()

View File

@@ -1,258 +0,0 @@
# Backport of OrderedDict() class that runs on Python 2.4, 2.5, 2.6, 2.7 and pypy.
# Passes Python2.7's test suite and incorporates all the latest updates.
try:
from thread import get_ident as _get_ident
except ImportError:
from dummy_thread import get_ident as _get_ident
try:
from _abcoll import KeysView, ValuesView, ItemsView
except ImportError:
pass
class OrderedDict(dict):
'Dictionary that remembers insertion order'
# An inherited dict maps keys to values.
# The inherited dict provides __getitem__, __len__, __contains__, and get.
# The remaining methods are order-aware.
# Big-O running times for all methods are the same as for regular dictionaries.
# The internal self.__map dictionary maps keys to links in a doubly linked list.
# The circular doubly linked list starts and ends with a sentinel element.
# The sentinel element never gets deleted (this simplifies the algorithm).
# Each link is stored as a list of length three: [PREV, NEXT, KEY].
def __init__(self, *args, **kwds):
'''Initialize an ordered dictionary. Signature is the same as for
regular dictionaries, but keyword arguments are not recommended
because their insertion order is arbitrary.
'''
if len(args) > 1:
raise TypeError('expected at most 1 arguments, got %d' % len(args))
try:
self.__root
except AttributeError:
self.__root = root = [] # sentinel node
root[:] = [root, root, None]
self.__map = {}
self.__update(*args, **kwds)
def __setitem__(self, key, value, dict_setitem=dict.__setitem__):
'od.__setitem__(i, y) <==> od[i]=y'
# Setting a new item creates a new link which goes at the end of the linked
# list, and the inherited dictionary is updated with the new key/value pair.
if key not in self:
root = self.__root
last = root[0]
last[1] = root[0] = self.__map[key] = [last, root, key]
dict_setitem(self, key, value)
def __delitem__(self, key, dict_delitem=dict.__delitem__):
'od.__delitem__(y) <==> del od[y]'
# Deleting an existing item uses self.__map to find the link which is
# then removed by updating the links in the predecessor and successor nodes.
dict_delitem(self, key)
link_prev, link_next, key = self.__map.pop(key)
link_prev[1] = link_next
link_next[0] = link_prev
def __iter__(self):
'od.__iter__() <==> iter(od)'
root = self.__root
curr = root[1]
while curr is not root:
yield curr[2]
curr = curr[1]
def __reversed__(self):
'od.__reversed__() <==> reversed(od)'
root = self.__root
curr = root[0]
while curr is not root:
yield curr[2]
curr = curr[0]
def clear(self):
'od.clear() -> None. Remove all items from od.'
try:
for node in self.__map.itervalues():
del node[:]
root = self.__root
root[:] = [root, root, None]
self.__map.clear()
except AttributeError:
pass
dict.clear(self)
def popitem(self, last=True):
'''od.popitem() -> (k, v), return and remove a (key, value) pair.
Pairs are returned in LIFO order if last is true or FIFO order if false.
'''
if not self:
raise KeyError('dictionary is empty')
root = self.__root
if last:
link = root[0]
link_prev = link[0]
link_prev[1] = root
root[0] = link_prev
else:
link = root[1]
link_next = link[1]
root[1] = link_next
link_next[0] = root
key = link[2]
del self.__map[key]
value = dict.pop(self, key)
return key, value
# -- the following methods do not depend on the internal structure --
def keys(self):
'od.keys() -> list of keys in od'
return list(self)
def values(self):
'od.values() -> list of values in od'
return [self[key] for key in self]
def items(self):
'od.items() -> list of (key, value) pairs in od'
return [(key, self[key]) for key in self]
def iterkeys(self):
'od.iterkeys() -> an iterator over the keys in od'
return iter(self)
def itervalues(self):
'od.itervalues -> an iterator over the values in od'
for k in self:
yield self[k]
def iteritems(self):
'od.iteritems -> an iterator over the (key, value) items in od'
for k in self:
yield (k, self[k])
def update(*args, **kwds):
'''od.update(E, **F) -> None. Update od from dict/iterable E and F.
If E is a dict instance, does: for k in E: od[k] = E[k]
If E has a .keys() method, does: for k in E.keys(): od[k] = E[k]
Or if E is an iterable of items, does: for k, v in E: od[k] = v
In either case, this is followed by: for k, v in F.items(): od[k] = v
'''
if len(args) > 2:
raise TypeError('update() takes at most 2 positional '
'arguments (%d given)' % (len(args),))
elif not args:
raise TypeError('update() takes at least 1 argument (0 given)')
self = args[0]
# Make progressively weaker assumptions about "other"
other = ()
if len(args) == 2:
other = args[1]
if isinstance(other, dict):
for key in other:
self[key] = other[key]
elif hasattr(other, 'keys'):
for key in other.keys():
self[key] = other[key]
else:
for key, value in other:
self[key] = value
for key, value in kwds.items():
self[key] = value
__update = update # let subclasses override update without breaking __init__
__marker = object()
def pop(self, key, default=__marker):
'''od.pop(k[,d]) -> v, remove specified key and return the corresponding value.
If key is not found, d is returned if given, otherwise KeyError is raised.
'''
if key in self:
result = self[key]
del self[key]
return result
if default is self.__marker:
raise KeyError(key)
return default
def setdefault(self, key, default=None):
'od.setdefault(k[,d]) -> od.get(k,d), also set od[k]=d if k not in od'
if key in self:
return self[key]
self[key] = default
return default
def __repr__(self, _repr_running={}):
'od.__repr__() <==> repr(od)'
call_key = id(self), _get_ident()
if call_key in _repr_running:
return '...'
_repr_running[call_key] = 1
try:
if not self:
return '%s()' % (self.__class__.__name__,)
return '%s(%r)' % (self.__class__.__name__, self.items())
finally:
del _repr_running[call_key]
def __reduce__(self):
'Return state information for pickling'
items = [[k, self[k]] for k in self]
inst_dict = vars(self).copy()
for k in vars(OrderedDict()):
inst_dict.pop(k, None)
if inst_dict:
return (self.__class__, (items,), inst_dict)
return self.__class__, (items,)
def copy(self):
'od.copy() -> a shallow copy of od'
return self.__class__(self)
@classmethod
def fromkeys(cls, iterable, value=None):
'''OD.fromkeys(S[, v]) -> New ordered dictionary with keys from S
and values equal to v (which defaults to None).
'''
d = cls()
for key in iterable:
d[key] = value
return d
def __eq__(self, other):
'''od.__eq__(y) <==> od==y. Comparison to another OD is order-sensitive
while comparison to a regular mapping is order-insensitive.
'''
if isinstance(other, OrderedDict):
return len(self)==len(other) and self.items() == other.items()
return dict.__eq__(self, other)
def __ne__(self, other):
return not self == other
# -- the following methods are only used in Python 2.7 --
def viewkeys(self):
"od.viewkeys() -> a set-like object providing a view on od's keys"
return KeysView(self)
def viewvalues(self):
"od.viewvalues() -> an object providing a view on od's values"
return ValuesView(self)
def viewitems(self):
"od.viewitems() -> a set-like object providing a view on od's items"
return ItemsView(self)

View File

@@ -1,62 +1,94 @@
import copy
import functools
import inspect
import os
import types
from collections import abc as collections_abc
from pathlib import Path
from . import filters, matchers
from .cassette import Cassette
from .serializers import yamlserializer, jsonserializer
from . import matchers
from .persisters.filesystem import FilesystemPersister
from .record_mode import RecordMode
from .serializers import jsonserializer, yamlserializer
from .util import auto_decorate, compose
class VCR(object):
def __init__(self,
serializer='yaml',
cassette_library_dir=None,
record_mode="once",
filter_headers=[],
filter_query_parameters=[],
before_record=None,
match_on=[
'method',
'scheme',
'host',
'port',
'path',
'query',
],
ignore_hosts=[],
ignore_localhost=False,
):
class VCR:
@staticmethod
def is_test_method(method_name, function):
return method_name.startswith("test") and isinstance(function, types.FunctionType)
@staticmethod
def ensure_suffix(suffix):
def ensure(path):
if not path.endswith(suffix):
return path + suffix
return path
return ensure
def __init__(
self,
path_transformer=None,
before_record_request=None,
custom_patches=(),
filter_query_parameters=(),
ignore_hosts=(),
record_mode=RecordMode.ONCE,
ignore_localhost=False,
filter_headers=(),
before_record_response=None,
filter_post_data_parameters=(),
match_on=("method", "scheme", "host", "port", "path", "query"),
before_record=None,
inject_cassette=False,
serializer="yaml",
cassette_library_dir=None,
func_path_generator=None,
decode_compressed_response=False,
record_on_exception=True,
drop_unused_requests=False,
):
self.serializer = serializer
self.match_on = match_on
self.cassette_library_dir = cassette_library_dir
self.serializers = {
'yaml': yamlserializer,
'json': jsonserializer,
}
self.serializers = {"yaml": yamlserializer, "json": jsonserializer}
self.matchers = {
'method': matchers.method,
'uri': matchers.uri,
'url': matchers.uri, # matcher for backwards compatibility
'scheme': matchers.scheme,
'host': matchers.host,
'port': matchers.port,
'path': matchers.path,
'query': matchers.query,
'headers': matchers.headers,
'body': matchers.body,
"method": matchers.method,
"uri": matchers.uri,
"url": matchers.uri, # matcher for backwards compatibility
"scheme": matchers.scheme,
"host": matchers.host,
"port": matchers.port,
"path": matchers.path,
"query": matchers.query,
"headers": matchers.headers,
"raw_body": matchers.raw_body,
"body": matchers.body,
}
self.persister = FilesystemPersister
self.record_mode = record_mode
self.filter_headers = filter_headers
self.filter_query_parameters = filter_query_parameters
self.before_record = before_record
self.filter_post_data_parameters = filter_post_data_parameters
self.before_record_request = before_record_request or before_record
self.before_record_response = before_record_response
self.ignore_hosts = ignore_hosts
self.ignore_localhost = ignore_localhost
self.inject_cassette = inject_cassette
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)
self.drop_unused_requests = drop_unused_requests
def _get_serializer(self, serializer_name):
try:
serializer = self.serializers[serializer_name]
except KeyError:
print("Serializer {0} doesn't exist or isn't registered".format(
serializer_name
))
raise KeyError
raise KeyError(f"Serializer {serializer_name} doesn't exist or isn't registered") from None
return serializer
def _get_matchers(self, matcher_names):
@@ -65,48 +97,165 @@ class VCR(object):
for m in matcher_names:
matchers.append(self.matchers[m])
except KeyError:
raise KeyError(
"Matcher {0} doesn't exist or isn't registered".format(
m)
)
raise KeyError(f"Matcher {m} doesn't exist or isn't registered") from None
return matchers
def use_cassette(self, path, **kwargs):
serializer_name = kwargs.get('serializer', self.serializer)
matcher_names = kwargs.get('match_on', self.match_on)
cassette_library_dir = kwargs.get(
'cassette_library_dir',
self.cassette_library_dir
)
def use_cassette(self, path=None, **kwargs):
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)
return self._use_cassette(path=path, **kwargs)
def _use_cassette(self, with_current_defaults=False, **kwargs):
if with_current_defaults:
config = self.get_merged_config(**kwargs)
return Cassette.use(**config)
# This is made a function that evaluates every time a cassette
# is made so that changes that are made to this VCR instance
# that occur AFTER the `use_cassette` decorator is applied
# still affect subsequent calls to the decorated function.
args_getter = functools.partial(self.get_merged_config, **kwargs)
return Cassette.use_arg_getter(args_getter)
def get_merged_config(self, **kwargs):
serializer_name = kwargs.get("serializer", self.serializer)
matcher_names = kwargs.get("match_on", self.match_on)
path_transformer = kwargs.get("path_transformer", self.path_transformer)
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:
path = os.path.join(cassette_library_dir, path)
def add_cassette_library_dir(path):
if not path.startswith(cassette_library_dir):
return os.path.join(cassette_library_dir, path)
return path
path_transformer = compose(add_cassette_library_dir, path_transformer)
elif not func_path_generator:
# If we don't have a library dir, use the functions
# location to build a full path for cassettes.
func_path_generator = self._build_path_from_func_using_module
merged_config = {
"serializer": self._get_serializer(serializer_name),
"match_on": self._get_matchers(matcher_names),
"record_mode": kwargs.get('record_mode', self.record_mode),
"filter_headers": kwargs.get(
'filter_headers', self.filter_headers
),
"filter_query_parameters": kwargs.get(
'filter_query_parameters', self.filter_query_parameters
),
"before_record": kwargs.get(
"before_record", self.before_record
),
"ignore_hosts": kwargs.get(
'ignore_hosts', self.ignore_hosts
),
"ignore_localhost": kwargs.get(
'ignore_localhost', self.ignore_localhost
),
"persister": self.persister,
"match_on": self._get_matchers(tuple(matcher_names) + tuple(additional_matchers)),
"record_mode": kwargs.get("record_mode", self.record_mode),
"before_record_request": self._build_before_record_request(kwargs),
"before_record_response": self._build_before_record_response(kwargs),
"custom_patches": self._custom_patches + kwargs.get("custom_patches", ()),
"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,
"drop_unused_requests": kwargs.get("drop_unused_requests", self.drop_unused_requests),
}
path = kwargs.get("path")
if path:
merged_config["path"] = path
return merged_config
return Cassette.load(path, **merged_config)
def _build_before_record_response(self, options):
before_record_response = options.get("before_record_response", self.before_record_response)
decode_compressed_response = options.get(
"decode_compressed_response",
self.decode_compressed_response,
)
filter_functions = []
if decode_compressed_response:
filter_functions.append(filters.decode_response)
if before_record_response:
if not isinstance(before_record_response, collections_abc.Iterable):
before_record_response = (before_record_response,)
filter_functions.extend(before_record_response)
def before_record_response(response):
for function in filter_functions:
if response is None:
break
response = function(response)
return response
return before_record_response
def _build_before_record_request(self, options):
filter_functions = []
filter_headers = options.get("filter_headers", self.filter_headers)
filter_query_parameters = options.get("filter_query_parameters", self.filter_query_parameters)
filter_post_data_parameters = options.get(
"filter_post_data_parameters",
self.filter_post_data_parameters,
)
before_record_request = options.get(
"before_record_request",
options.get("before_record", self.before_record_request),
)
ignore_hosts = options.get("ignore_hosts", self.ignore_hosts)
ignore_localhost = options.get("ignore_localhost", self.ignore_localhost)
if filter_headers:
replacements = [h if isinstance(h, tuple) else (h, None) for h in filter_headers]
filter_functions.append(functools.partial(filters.replace_headers, replacements=replacements))
if filter_query_parameters:
replacements = [p if isinstance(p, tuple) else (p, None) for p in filter_query_parameters]
filter_functions.append(
functools.partial(filters.replace_query_parameters, replacements=replacements),
)
if filter_post_data_parameters:
replacements = [p if isinstance(p, tuple) else (p, None) for p in filter_post_data_parameters]
filter_functions.append(
functools.partial(filters.replace_post_data_parameters, replacements=replacements),
)
hosts_to_ignore = set(ignore_hosts)
if ignore_localhost:
hosts_to_ignore.update(("localhost", "0.0.0.0", "127.0.0.1"))
if hosts_to_ignore:
filter_functions.append(self._build_ignore_hosts(hosts_to_ignore))
if before_record_request:
if not isinstance(before_record_request, collections_abc.Iterable):
before_record_request = (before_record_request,)
filter_functions.extend(before_record_request)
def before_record_request(request):
request = copy.deepcopy(request)
for function in filter_functions:
if request is None:
break
request = function(request)
return request
return before_record_request
@staticmethod
def _build_ignore_hosts(hosts_to_ignore):
def filter_ignored_hosts(request):
if hasattr(request, "host") and request.host in hosts_to_ignore:
return
return request
return filter_ignored_hosts
@staticmethod
def _build_path_from_func_using_module(function):
return os.path.join(os.path.dirname(inspect.getfile(function)), function.__name__)
def register_serializer(self, name, serializer):
self.serializers[name] = serializer
def register_matcher(self, name, matcher):
self.matchers[name] = matcher
def register_persister(self, persister):
# Singleton, no name required
self.persister = persister
def test_case(self, predicate=None):
predicate = predicate or self.is_test_method
metaclass = auto_decorate(self.use_cassette, predicate)
return metaclass("temporary_class", (), {})

View File

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

View File

@@ -1,43 +1,180 @@
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 _remove_headers(request, headers_to_remove):
headers = copy.copy(request.headers)
headers_to_remove = [h.lower() for h in headers_to_remove]
keys = [k for k in headers if k.lower() in headers_to_remove]
if keys:
for k in keys:
headers.pop(k)
request.headers = headers
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.
"""
new_headers = request.headers.copy()
for k, rv in replacements:
if k in new_headers:
ov = new_headers.pop(k)
if callable(rv):
rv = rv(key=k, value=ov, request=request)
if rv is not None:
new_headers[k] = rv
request.headers = new_headers
return request
def _remove_query_parameters(request, query_parameters_to_remove):
def remove_headers(request, headers_to_remove):
"""
Wrap replace_headers() for API backward compatibility.
"""
replacements = [(k, None) for k in headers_to_remove]
return replace_headers(request, replacements)
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:
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.
"""
query = request.query
new_query = [(k, v) for (k, v) in query
if k not in query_parameters_to_remove]
if len(new_query) != len(query):
uri_parts = list(urlparse(request.uri))
uri_parts[4] = urlencode(new_query)
request.uri = urlunparse(uri_parts)
new_query = []
replacements = dict(replacements)
for k, ov in query:
if k not in replacements:
new_query.append((k, ov))
else:
rv = replacements[k]
if callable(rv):
rv = rv(key=k, value=ov, request=request)
if rv is not None:
new_query.append((k, rv))
uri_parts = list(urlparse(request.uri))
uri_parts[4] = urlencode(new_query)
request.uri = urlunparse(uri_parts)
return request
def filter_request(
request,
filter_headers,
filter_query_parameters,
before_record,
ignore_hosts
):
request = copy.copy(request) # don't mutate request object
if hasattr(request, 'headers') and filter_headers:
request = _remove_headers(request, filter_headers)
if hasattr(request, 'host') and request.host in ignore_hosts:
return None
if filter_query_parameters:
request = _remove_query_parameters(request, filter_query_parameters)
if before_record:
request = before_record(request)
def remove_query_parameters(request, query_parameters_to_remove):
"""
Wrap replace_query_parameters() for API backward compatibility.
"""
replacements = [(k, None) for k in query_parameters_to_remove]
return replace_query_parameters(request, replacements)
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:
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.
"""
if not request.body:
# Nothing to replace
return request
replacements = dict(replacements)
if request.method == "POST" and not isinstance(request.body, BytesIO):
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)
for k, rv in replacements.items():
if k in json_data:
ov = json_data.pop(k)
if callable(rv):
rv = rv(key=k, value=ov, request=request)
if rv is not None:
json_data[k] = rv
request.body = json.dumps(json_data).encode("utf-8")
else:
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 = []
for k, sep, ov in splits:
if sep is None:
new_splits.append((k, sep, ov))
else:
rk = k.decode("utf-8")
if rk not in replacements:
new_splits.append((k, sep, ov))
else:
rv = replacements[rk]
if callable(rv):
rv = rv(key=rk, value=ov.decode("utf-8"), request=request)
if rv is not None:
new_splits.append((k, sep, rv.encode("utf-8")))
request.body = b"&".join(k if sep is None else b"".join([k, sep, v]) for k, sep, v in new_splits)
return request
def remove_post_data_parameters(request, post_data_parameters_to_remove):
"""
Wrap replace_post_data_parameters() for API backward compatibility.
"""
replacements = [(k, None) for k in post_data_parameters_to_remove]
return replace_post_data_parameters(request, replacements)
def decode_response(response):
"""
If the response is compressed with gzip or deflate:
1. decompress the response body
2. delete the content-encoding header
3. update content-length header to decompressed length
"""
def is_compressed(headers):
encoding = headers.get("content-encoding", [])
return encoding and encoding[0] in ("gzip", "deflate")
def decompress_body(body, encoding):
"""Returns decompressed body according to encoding using zlib.
to (de-)compress gzip format, use wbits = zlib.MAX_WBITS | 16
"""
if not body:
return ""
if encoding == "gzip":
try:
return zlib.decompress(body, zlib.MAX_WBITS | 16)
except zlib.error:
return body # assumes that the data was already decompressed
else: # encoding == 'deflate'
try:
return zlib.decompress(body)
except zlib.error:
return body # assumes that the data was already decompressed
# Deepcopy here in case `headers` contain objects that could
# be mutated by a shallow copy and corrupt the real response.
response = copy.deepcopy(response)
headers = CaseInsensitiveDict(response["headers"])
if is_compressed(headers):
encoding = headers["content-encoding"][0]
headers["content-encoding"].remove(encoding)
if not headers["content-encoding"]:
del headers["content-encoding"]
new_body = decompress_body(response["body"]["string"], encoding)
response["body"]["string"] = new_body
headers["content-length"] = [str(len(new_body))]
response["headers"] = dict(headers)
return response

View File

@@ -1,53 +1,210 @@
import json
import logging
import urllib
import xmlrpc.client
from string import hexdigits
from .util import read_body
_HEXDIG_CODE_POINTS: set[int] = {ord(s.encode("ascii")) for s in hexdigits}
log = logging.getLogger(__name__)
def method(r1, r2):
return r1.method == r2.method
if r1.method != r2.method:
raise AssertionError(f"{r1.method} != {r2.method}")
def uri(r1, r2):
return r1.uri == r2.uri
if r1.uri != r2.uri:
raise AssertionError(f"{r1.uri} != {r2.uri}")
def host(r1, r2):
return r1.host == r2.host
if r1.host != r2.host:
raise AssertionError(f"{r1.host} != {r2.host}")
def scheme(r1, r2):
return r1.scheme == r2.scheme
if r1.scheme != r2.scheme:
raise AssertionError(f"{r1.scheme} != {r2.scheme}")
def port(r1, r2):
return r1.port == r2.port
if r1.port != r2.port:
raise AssertionError(f"{r1.port} != {r2.port}")
def path(r1, r2):
return r1.path == r2.path
if r1.path != r2.path:
raise AssertionError(f"{r1.path} != {r2.path}")
def query(r1, r2):
return r1.query == r2.query
if r1.query != r2.query:
raise AssertionError(f"{r1.query} != {r2.query}")
def raw_body(r1, r2):
if read_body(r1) != read_body(r2):
raise AssertionError
def body(r1, r2):
return r1.body == r2.body
transformers = list(_get_transformers(r1))
if transformers != list(_get_transformers(r2)):
transformers = []
b1 = read_body(r1)
b2 = read_body(r2)
for transform in transformers:
b1 = transform(b1)
b2 = transform(b2)
if b1 != b2:
raise AssertionError
def headers(r1, r2):
return r1.headers == r2.headers
if r1.headers != r2.headers:
raise AssertionError(f"{r1.headers} != {r2.headers}")
def _log_matches(matches):
differences = [m for m in matches if not m[0]]
if differences:
log.debug(
'Requests differ according to the following matchers: ' +
str(differences)
)
def _header_checker(value, header="Content-Type"):
def checker(headers):
_header = headers.get(header, "")
if isinstance(_header, bytes):
_header = _header.decode("utf-8")
return value in _header.lower()
return checker
def _dechunk(body):
if isinstance(body, str):
body = body.encode("utf-8")
elif isinstance(body, bytearray):
body = bytes(body)
elif hasattr(body, "__iter__"):
body = list(body)
if body:
if isinstance(body[0], str):
body = ("".join(body)).encode("utf-8")
elif isinstance(body[0], bytes):
body = b"".join(body)
elif isinstance(body[0], int):
body = bytes(body)
else:
raise ValueError(f"Body chunk type {type(body[0])} not supported")
else:
body = None
if not isinstance(body, bytes):
return body
# Now decode chunked data format (https://en.wikipedia.org/wiki/Chunked_transfer_encoding)
# Example input: b"45\r\n<69 bytes>\r\n0\r\n\r\n" where int(b"45", 16) == 69.
CHUNK_GAP = b"\r\n"
BODY_LEN: int = len(body)
chunks: list[bytes] = []
pos: int = 0
while True:
for i in range(pos, BODY_LEN):
if body[i] not in _HEXDIG_CODE_POINTS:
break
if i == 0 or body[i : i + len(CHUNK_GAP)] != CHUNK_GAP:
if pos == 0:
return body # i.e. assume non-chunk data
raise ValueError("Malformed chunked data")
size_bytes = int(body[pos:i], 16)
if size_bytes == 0: # i.e. well-formed ending
return b"".join(chunks)
chunk_data_first = i + len(CHUNK_GAP)
chunk_data_after_last = chunk_data_first + size_bytes
if body[chunk_data_after_last : chunk_data_after_last + len(CHUNK_GAP)] != CHUNK_GAP:
raise ValueError("Malformed chunked data")
chunk_data = body[chunk_data_first:chunk_data_after_last]
chunks.append(chunk_data)
pos = chunk_data_after_last + len(CHUNK_GAP)
def _transform_json(body):
if body:
return json.loads(body)
_xml_header_checker = _header_checker("text/xml")
_xmlrpc_header_checker = _header_checker("xmlrpc", header="User-Agent")
_checker_transformer_pairs = (
(_header_checker("chunked", header="Transfer-Encoding"), _dechunk),
(
_header_checker("application/x-www-form-urlencoded"),
lambda body: urllib.parse.parse_qs(body.decode("ascii")),
),
(_header_checker("application/json"), _transform_json),
(lambda request: _xml_header_checker(request) and _xmlrpc_header_checker(request), xmlrpc.client.loads),
)
def _get_transformers(request):
for checker, transformer in _checker_transformer_pairs:
if checker(request.headers):
yield transformer
def requests_match(r1, r2, matchers):
matches = [(m(r1, r2), m) for m in matchers]
_log_matches(matches)
return all([m[0] for m in matches])
_, failures = get_matchers_results(r1, r2, matchers)
if failures:
log.debug(f"Requests {r1} and {r2} differ.\nFailure details:\n{failures}")
return len(failures) == 0
def _evaluate_matcher(matcher_function, *args):
"""
Evaluate the result of a given matcher as a boolean with an assertion error message if any.
It handles two types of matcher :
- a matcher returning a boolean value.
- a matcher that only makes an assert, returning None or raises an assertion error.
"""
assertion_message = None
try:
match = matcher_function(*args)
match = True if match is None else match
except AssertionError as e:
match = False
assertion_message = str(e)
return match, assertion_message
def get_matchers_results(r1, r2, matchers):
"""
Get the comparison results of two requests as two list.
The first returned list represents the matchers names that passed.
The second list is the failed matchers as a string with failed assertion details if any.
"""
matches_success, matches_fails = [], []
for m in matchers:
matcher_name = m.__name__
match, assertion_message = _evaluate_matcher(m, r1, r2)
if match:
matches_success.append(matcher_name)
else:
assertion_message = get_assertion_message(assertion_message)
matches_fails.append((matcher_name, assertion_message))
return matches_success, matches_fails
def get_assertion_message(assertion_details):
"""
Get a detailed message about the failing matcher.
"""
return assertion_details

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 compat, 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
@@ -38,55 +39,46 @@ def preprocess_yaml(cassette):
# versions. So this just strips the tags before deserializing.
STRINGS_TO_NUKE = [
'!!python/object:vcr.request.Request',
'!!python/object/apply:__builtin__.frozenset',
'!!python/object/apply:builtins.frozenset',
"!!python/object:vcr.request.Request",
"!!python/object/apply:__builtin__.frozenset",
"!!python/object/apply:builtins.frozenset",
]
for s in STRINGS_TO_NUKE:
cassette = cassette.replace(s, '')
cassette = cassette.replace(s, "")
return cassette
PARTS = [
'protocol',
'host',
'port',
'path',
]
PARTS = ["protocol", "host", "port", "path"]
def build_uri(**parts):
port = parts['port']
scheme = parts['protocol']
default_port = {'https': 433, 'http': 80}[scheme]
parts['port'] = ':{0}'.format(port) if port != default_port else ''
port = parts["port"]
scheme = parts["protocol"]
default_port = {"https": 443, "http": 80}[scheme]
parts["port"] = f":{port}" if port != default_port else ""
return "{protocol}://{host}{port}{path}".format(**parts)
def _migrate(data):
interactions = []
for item in data:
req = item['request']
res = item['response']
uri = dict((k, req.pop(k)) for k in PARTS)
req['uri'] = build_uri(**uri)
req = item["request"]
res = item["response"]
uri = {k: req.pop(k) for k in PARTS}
req["uri"] = build_uri(**uri)
# convert headers to dict of lists
headers = req['headers']
headers = req["headers"]
for k in headers:
headers[k] = [headers[k]]
response_headers = {}
for k, v in get_httpmessage(
b"".join(h.encode('utf-8') for h in res['headers'])
).items():
for k, v in get_httpmessage(b"".join(h.encode("utf-8") for h in res["headers"])).items():
response_headers.setdefault(k, [])
response_headers[k].append(v)
res['headers'] = response_headers
interactions.append({'request': req, 'response': res})
res["headers"] = response_headers
interactions.append({"request": req, "response": res})
return {
'requests': [
request.Request._from_dict(i['request']) for i in interactions
],
'responses': [i['response'] for i in interactions],
"requests": [request.Request._from_dict(i["request"]) for i in interactions],
"responses": [i["response"] for i in interactions],
}
@@ -100,12 +92,12 @@ def migrate_json(in_fp, out_fp):
def _list_of_tuples_to_dict(fs):
return dict((k, v) for k, v in fs[0])
return dict(fs[0])
def _already_migrated(data):
try:
if data.get('version') == 1:
if data.get("version") == 1:
return True
except AttributeError:
return False
@@ -116,9 +108,7 @@ def migrate_yml(in_fp, out_fp):
if _already_migrated(data):
return False
for i in range(len(data)):
data[i]['request']['headers'] = _list_of_tuples_to_dict(
data[i]['request']['headers']
)
data[i]["request"]["headers"] = _list_of_tuples_to_dict(data[i]["request"]["headers"])
interactions = _migrate(data)
out_fp.write(serialize(interactions, yamlserializer))
return True
@@ -127,42 +117,42 @@ def migrate_yml(in_fp, out_fp):
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 tempfile.TemporaryFile(mode="w+") as out_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:
with open(file_path, "w") as in_fp:
out_fp.seek(0)
shutil.copyfileobj(out_fp, in_fp)
return True
def try_migrate(path):
if path.endswith('.json'):
if path.endswith(".json"):
return migrate(path, migrate_json)
elif path.endswith('.yaml') or path.endswith('.yml'):
elif path.endswith((".yaml", ".yml")):
return migrate(path, migrate_yml)
return False
def main():
if len(sys.argv) != 2:
raise SystemExit("Please provide path to cassettes directory or file. "
"Usage: python -m vcr.migration PATH")
raise SystemExit(
"Please provide path to cassettes directory or file. Usage: python3 -m vcr.migration PATH",
)
path = sys.argv[1]
if not os.path.isabs(path):
path = os.path.abspath(path)
files = [path]
if os.path.isdir(path):
files = (os.path.join(root, name)
for (root, dirs, files) in os.walk(path)
for name in files)
files = (os.path.join(root, name) for (root, dirs, files) in os.walk(path) for name in files)
for file_path in files:
migrated = try_migrate(file_path)
status = 'OK' if migrated else 'FAIL'
sys.stderr.write("[{0}] {1}\n".format(status, file_path))
migrated = try_migrate(file_path)
status = "OK" if migrated else "FAIL"
sys.stderr.write(f"[{status}] {file_path}\n")
sys.stderr.write("Done.\n")
if __name__ == '__main__':
if __name__ == "__main__":
main()

View File

@@ -1,145 +1,456 @@
'''Utilities for patching in cassettes'''
"""Utilities for patching in cassettes"""
import contextlib
import functools
import http.client as httplib
import itertools
import logging
from unittest import mock
from .stubs import VCRHTTPConnection, VCRHTTPSConnection
from six.moves import http_client as httplib
log = logging.getLogger(__name__)
# Save some of the original types for the purposes of unpatching
_HTTPConnection = httplib.HTTPConnection
_HTTPSConnection = httplib.HTTPSConnection
# Try to save the original types for boto3
try:
# Try to save the original types for requests
import requests.packages.urllib3.connectionpool as cpool
_VerifiedHTTPSConnection = cpool.VerifiedHTTPSConnection
_cpoolHTTPConnection = cpool.HTTPConnection
_cpoolHTTPSConnection = cpool.HTTPSConnection
from botocore.awsrequest import AWSHTTPConnection, AWSHTTPSConnection
except ImportError as e:
try:
import botocore.vendored.requests # noqa: F401
except ImportError: # pragma: no cover
pass
else:
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 = conn.VerifiedHTTPSConnection
_connHTTPConnection = conn.HTTPConnection
_connHTTPSConnection = conn.HTTPSConnection
# Try to save the original types for requests
try:
# Try to save the original types for urllib3
import urllib3
_VerifiedHTTPSConnection = urllib3.connectionpool.VerifiedHTTPSConnection
import requests
except ImportError: # pragma: no cover
pass
else:
if requests.__build__ < 0x021602:
raise RuntimeError(
"vcrpy >=4.2.2 and requests <2.16.2 are not compatible"
"; please upgrade requests (or downgrade vcrpy)",
)
# Try to save the original types for httplib2
try:
# Try to save the original types for httplib2
import httplib2
except ImportError: # pragma: no cover
pass
else:
_HTTPConnectionWithTimeout = httplib2.HTTPConnectionWithTimeout
_HTTPSConnectionWithTimeout = httplib2.HTTPSConnectionWithTimeout
_SCHEME_TO_CONNECTION = httplib2.SCHEME_TO_CONNECTION
# Try to save the original types for Tornado
try:
import tornado.simple_httpclient
except ImportError: # pragma: no cover
pass
else:
_SimpleAsyncHTTPClient_fetch_impl = tornado.simple_httpclient.SimpleAsyncHTTPClient.fetch_impl
try:
# Try to save the original types for boto
import boto.https_connection
_CertValidatingHTTPSConnection = \
boto.https_connection.CertValidatingHTTPSConnection
import tornado.curl_httpclient
except ImportError: # pragma: no cover
pass
else:
_CurlAsyncHTTPClient_fetch_impl = tornado.curl_httpclient.CurlAsyncHTTPClient.fetch_impl
try:
import aiohttp.client
except ImportError: # pragma: no cover
pass
else:
_AiohttpClientSessionRequest = aiohttp.client.ClientSession._request
def install(cassette):
"""
Patch all the HTTPConnections references we can find!
This replaces the actual HTTPConnection with a VCRHTTPConnection
object which knows how to save to / read from cassettes
"""
httplib.HTTPConnection = VCRHTTPConnection
httplib.HTTPSConnection = VCRHTTPSConnection
httplib.HTTPConnection.cassette = cassette
httplib.HTTPSConnection.cassette = cassette
try:
import httpcore
except ImportError: # pragma: no cover
pass
else:
_HttpcoreConnectionPool_handle_request = httpcore.ConnectionPool.handle_request
_HttpcoreAsyncConnectionPool_handle_async_request = httpcore.AsyncConnectionPool.handle_async_request
class CassettePatcherBuilder:
def _build_patchers_from_mock_triples_decorator(function):
@functools.wraps(function)
def wrapped(self, *args, **kwargs):
return self._build_patchers_from_mock_triples(function(self, *args, **kwargs))
return wrapped
def __init__(self, cassette):
self._cassette = cassette
self._class_to_cassette_subclass = {}
def build(self):
return itertools.chain(
self._httplib(),
self._requests(),
self._boto3(),
self._urllib3(),
self._httplib2(),
self._tornado(),
self._aiohttp(),
self._httpcore(),
self._build_patchers_from_mock_triples(self._cassette.custom_patches),
)
def _build_patchers_from_mock_triples(self, mock_triples):
for args in mock_triples:
patcher = self._build_patcher(*args)
if patcher:
yield patcher
def _build_patcher(self, obj, patched_attribute, replacement_class):
if not hasattr(obj, patched_attribute):
return
return mock.patch.object(
obj,
patched_attribute,
self._recursively_apply_get_cassette_subclass(replacement_class),
)
def _recursively_apply_get_cassette_subclass(self, replacement_dict_or_obj):
"""One of the subtleties of this class is that it does not directly
replace HTTPSConnection with `VCRRequestsHTTPSConnection`, but a
subclass of the aforementioned class that has the `cassette`
class attribute assigned to `self._cassette`. This behavior is
necessary to properly support nested cassette contexts.
This function exists to ensure that we use the same class
object (reference) to patch everything that replaces
VCRRequestHTTP[S]Connection, but that we can talk about
patching them with the raw references instead, and without
worrying about exactly where the subclass with the relevant
value for `cassette` is first created.
The function is recursive because it looks in to dictionaries
and replaces class values at any depth with the subclass
described in the previous paragraph.
"""
if isinstance(replacement_dict_or_obj, dict):
for key, replacement_obj in replacement_dict_or_obj.items():
replacement_obj = self._recursively_apply_get_cassette_subclass(replacement_obj)
replacement_dict_or_obj[key] = replacement_obj
return replacement_dict_or_obj
if hasattr(replacement_dict_or_obj, "cassette"):
replacement_dict_or_obj = self._get_cassette_subclass(replacement_dict_or_obj)
return replacement_dict_or_obj
def _get_cassette_subclass(self, klass):
if klass.cassette is not None:
return klass
if klass not in self._class_to_cassette_subclass:
subclass = self._build_cassette_subclass(klass)
self._class_to_cassette_subclass[klass] = subclass
return self._class_to_cassette_subclass[klass]
def _build_cassette_subclass(self, base_class):
bases = (base_class,)
if not issubclass(base_class, object): # Check for old style class
bases += (object,)
return type(f"{base_class.__name__}{self._cassette._path}", bases, {"cassette": self._cassette})
@_build_patchers_from_mock_triples_decorator
def _httplib(self):
yield httplib, "HTTPConnection", VCRHTTPConnection
yield httplib, "HTTPSConnection", VCRHTTPSConnection
def _requests(self):
try:
from .stubs import requests_stubs
except ImportError: # pragma: no cover
return ()
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
pass
else:
from .stubs import boto3_stubs
log.debug("Patching boto3 cpool with %s", cpool)
yield cpool.AWSHTTPConnectionPool, "ConnectionCls", boto3_stubs.VCRRequestsHTTPConnection
yield cpool.AWSHTTPSConnectionPool, "ConnectionCls", boto3_stubs.VCRRequestsHTTPSConnection
def _patched_get_conn(self, connection_pool_class, connection_class_getter):
get_conn = connection_pool_class._get_conn
@functools.wraps(get_conn)
def patched_get_conn(pool, timeout=None):
connection = get_conn(pool, timeout)
connection_class = (
pool.ConnectionCls if hasattr(pool, "ConnectionCls") else connection_class_getter()
)
# We need to make sure that we are actually providing a
# patched version of the connection class. This might not
# always be the case because the pool keeps previously
# used connections (which might actually be of a different
# class) around. This while loop will terminate because
# eventually the pool will run out of connections.
while not isinstance(connection, connection_class):
connection = get_conn(pool, timeout)
return connection
return patched_get_conn
def _patched_new_conn(self, connection_pool_class, connection_remover):
new_conn = connection_pool_class._new_conn
@functools.wraps(new_conn)
def patched_new_conn(pool):
new_connection = new_conn(pool)
connection_remover.add_connection_to_pool_entry(pool, new_connection)
return new_connection
return patched_new_conn
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, conn, urllib3_stubs)
@_build_patchers_from_mock_triples_decorator
def _httplib2(self):
try:
import httplib2 as cpool
except ImportError: # pragma: no cover
pass
else:
from .stubs.httplib2_stubs import VCRHTTPConnectionWithTimeout, VCRHTTPSConnectionWithTimeout
yield cpool, "HTTPConnectionWithTimeout", VCRHTTPConnectionWithTimeout
yield cpool, "HTTPSConnectionWithTimeout", VCRHTTPSConnectionWithTimeout
yield (
cpool,
"SCHEME_TO_CONNECTION",
{
"http": VCRHTTPConnectionWithTimeout,
"https": VCRHTTPSConnectionWithTimeout,
},
)
@_build_patchers_from_mock_triples_decorator
def _tornado(self):
try:
import tornado.simple_httpclient as simple
except ImportError: # pragma: no cover
pass
else:
from .stubs.tornado_stubs import vcr_fetch_impl
new_fetch_impl = vcr_fetch_impl(self._cassette, _SimpleAsyncHTTPClient_fetch_impl)
yield simple.SimpleAsyncHTTPClient, "fetch_impl", new_fetch_impl
try:
import tornado.curl_httpclient as curl
except ImportError: # pragma: no cover
pass
else:
from .stubs.tornado_stubs import vcr_fetch_impl
new_fetch_impl = vcr_fetch_impl(self._cassette, _CurlAsyncHTTPClient_fetch_impl)
yield curl.CurlAsyncHTTPClient, "fetch_impl", new_fetch_impl
@_build_patchers_from_mock_triples_decorator
def _aiohttp(self):
try:
import aiohttp.client as client
except ImportError: # pragma: no cover
pass
else:
from .stubs.aiohttp_stubs import vcr_request
new_request = vcr_request(self._cassette, _AiohttpClientSessionRequest)
yield client.ClientSession, "_request", new_request
@_build_patchers_from_mock_triples_decorator
def _httpcore(self):
try:
import httpcore
except ImportError: # pragma: no cover
return
else:
from .stubs.httpcore_stubs import vcr_handle_async_request, vcr_handle_request
new_handle_async_request = vcr_handle_async_request(
self._cassette,
_HttpcoreAsyncConnectionPool_handle_async_request,
)
yield httpcore.AsyncConnectionPool, "handle_async_request", new_handle_async_request
new_handle_request = vcr_handle_request(self._cassette, _HttpcoreConnectionPool_handle_request)
yield httpcore.ConnectionPool, "handle_request", new_handle_request
def _urllib3_patchers(self, cpool, conn, stubs):
http_connection_remover = ConnectionRemover(
self._get_cassette_subclass(stubs.VCRRequestsHTTPConnection),
)
https_connection_remover = ConnectionRemover(
self._get_cassette_subclass(stubs.VCRRequestsHTTPSConnection),
)
mock_triples = (
(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),
)
# These handle making sure that sessions only use the
# connections of the appropriate type.
mock_triples += (
(
cpool.HTTPConnectionPool,
"_get_conn",
self._patched_get_conn(cpool.HTTPConnectionPool, lambda: cpool.HTTPConnection),
),
(
cpool.HTTPSConnectionPool,
"_get_conn",
self._patched_get_conn(cpool.HTTPSConnectionPool, lambda: cpool.HTTPSConnection),
),
(
cpool.HTTPConnectionPool,
"_new_conn",
self._patched_new_conn(cpool.HTTPConnectionPool, http_connection_remover),
),
(
cpool.HTTPSConnectionPool,
"_new_conn",
self._patched_new_conn(cpool.HTTPSConnectionPool, https_connection_remover),
),
)
return itertools.chain(
self._build_patchers_from_mock_triples(mock_triples),
(http_connection_remover, https_connection_remover),
)
class ConnectionRemover:
def __init__(self, connection_class):
self._connection_class = connection_class
self._connection_pool_to_connections = {}
def add_connection_to_pool_entry(self, pool, connection):
if isinstance(connection, self._connection_class):
self._connection_pool_to_connections.setdefault(pool, set()).add(connection)
def __enter__(self):
return self
def __exit__(self, *args):
for pool, connections in self._connection_pool_to_connections.items():
readd_connections = []
while pool.pool and not pool.pool.empty() and connections:
connection = pool.pool.get()
if isinstance(connection, self._connection_class):
connections.remove(connection)
connection.close()
else:
readd_connections.append(connection)
for connection in readd_connections:
pool._put_conn(connection)
for connection in connections:
connection.close()
def reset_patchers():
yield mock.patch.object(httplib, "HTTPConnection", _HTTPConnection)
yield mock.patch.object(httplib, "HTTPSConnection", _HTTPSConnection)
# patch requests v1.x
try:
import requests.packages.urllib3.connectionpool as cpool
from .stubs.requests_stubs import VCRVerifiedHTTPSConnection
cpool.VerifiedHTTPSConnection = VCRVerifiedHTTPSConnection
cpool.VerifiedHTTPSConnection.cassette = cassette
cpool.HTTPConnection = VCRHTTPConnection
cpool.HTTPConnection.cassette = cassette
# patch requests v2.x
cpool.HTTPConnectionPool.ConnectionCls = VCRHTTPConnection
cpool.HTTPConnectionPool.cassette = cassette
cpool.HTTPSConnectionPool.ConnectionCls = VCRHTTPSConnection
cpool.HTTPSConnectionPool.cassette = cassette
except ImportError: # pragma: no cover
pass
# patch urllib3
try:
import urllib3.connection as conn
import urllib3.connectionpool as cpool
from .stubs.urllib3_stubs import VCRVerifiedHTTPSConnection
cpool.VerifiedHTTPSConnection = VCRVerifiedHTTPSConnection
cpool.VerifiedHTTPSConnection.cassette = cassette
cpool.HTTPConnection = VCRHTTPConnection
cpool.HTTPConnection.cassette = cassette
except ImportError: # pragma: no cover
pass
# patch httplib2
try:
import httplib2 as cpool
from .stubs.httplib2_stubs import VCRHTTPConnectionWithTimeout
from .stubs.httplib2_stubs import VCRHTTPSConnectionWithTimeout
cpool.HTTPConnectionWithTimeout = VCRHTTPConnectionWithTimeout
cpool.HTTPSConnectionWithTimeout = VCRHTTPSConnectionWithTimeout
cpool.SCHEME_TO_CONNECTION = {
'http': VCRHTTPConnectionWithTimeout,
'https': VCRHTTPSConnectionWithTimeout
}
except ImportError: # pragma: no cover
pass
# patch boto
try:
import boto.https_connection as cpool
from .stubs.boto_stubs import VCRCertValidatingHTTPSConnection
cpool.CertValidatingHTTPSConnection = VCRCertValidatingHTTPSConnection
cpool.CertValidatingHTTPSConnection.cassette = cassette
except ImportError: # pragma: no cover
pass
def reset():
'''Undo all the patching'''
httplib.HTTPConnection = _HTTPConnection
httplib.HTTPSConnection = _HTTPSConnection
try:
import requests.packages.urllib3.connectionpool as cpool
# unpatch requests v1.x
cpool.VerifiedHTTPSConnection = _VerifiedHTTPSConnection
cpool.HTTPConnection = _cpoolHTTPConnection
# unpatch requests v2.x
cpool.HTTPConnectionPool.ConnectionCls = _cpoolHTTPConnection
cpool.HTTPSConnection = _cpoolHTTPSConnection
cpool.HTTPSConnectionPool.ConnectionCls = _cpoolHTTPSConnection
except ImportError: # pragma: no cover
pass
else:
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", _connHTTPConnection)
yield mock.patch.object(cpool.HTTPSConnectionPool, "ConnectionCls", _connHTTPSConnection)
try:
import urllib3.connectionpool as cpool
cpool.VerifiedHTTPSConnection = _VerifiedHTTPSConnection
cpool.HTTPConnection = _HTTPConnection
cpool.HTTPSConnection = _HTTPSConnection
cpool.HTTPConnectionPool.ConnectionCls = _HTTPConnection
cpool.HTTPSConnectionPool.ConnectionCls = _HTTPSConnection
# unpatch botocore with awsrequest
import botocore.awsrequest as cpool
except ImportError: # pragma: no cover
pass
else:
if hasattr(cpool.AWSHTTPConnectionPool, "ConnectionCls"):
yield mock.patch.object(cpool.AWSHTTPConnectionPool, "ConnectionCls", _cpoolBoto3HTTPConnection)
yield mock.patch.object(cpool.AWSHTTPSConnectionPool, "ConnectionCls", _cpoolBoto3HTTPSConnection)
if hasattr(cpool, "AWSHTTPSConnection"):
yield mock.patch.object(cpool, "AWSHTTPSConnection", _cpoolBoto3HTTPSConnection)
try:
import httplib2 as cpool
cpool.HTTPConnectionWithTimeout = _HTTPConnectionWithTimeout
cpool.HTTPSConnectionWithTimeout = _HTTPSConnectionWithTimeout
cpool.SCHEME_TO_CONNECTION = _SCHEME_TO_CONNECTION
except ImportError: # pragma: no cover
pass
else:
yield mock.patch.object(cpool, "HTTPConnectionWithTimeout", _HTTPConnectionWithTimeout)
yield mock.patch.object(cpool, "HTTPSConnectionWithTimeout", _HTTPSConnectionWithTimeout)
yield mock.patch.object(cpool, "SCHEME_TO_CONNECTION", _SCHEME_TO_CONNECTION)
try:
import boto.https_connection as cpool
cpool.CertValidatingHTTPSConnection = _CertValidatingHTTPSConnection
import tornado.simple_httpclient as simple
except ImportError: # pragma: no cover
pass
else:
yield mock.patch.object(simple.SimpleAsyncHTTPClient, "fetch_impl", _SimpleAsyncHTTPClient_fetch_impl)
try:
import tornado.curl_httpclient as curl
except ImportError: # pragma: no cover
pass
else:
yield mock.patch.object(curl.CurlAsyncHTTPClient, "fetch_impl", _CurlAsyncHTTPClient_fetch_impl)
@contextlib.contextmanager
def force_reset():
with contextlib.ExitStack() as exit_stack:
for patcher in reset_patchers():
exit_stack.enter_context(patcher)
yield

View File

@@ -1,14 +0,0 @@
from .persisters.filesystem import FilesystemPersister
from .serialize import serialize, deserialize
def load_cassette(cassette_path, serializer):
with open(cassette_path) as f:
cassette_content = f.read()
cassette = deserialize(cassette_content, serializer)
return cassette
def save_cassette(cassette_path, cassette_dict, serializer):
data = serialize(cassette_dict, serializer)
FilesystemPersister.write(cassette_path, data)

Some files were not shown because too many files have changed in this diff Show More